diff --git a/composer.json b/composer.json index d1c4bc5..98e408d 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,10 @@ ] }, "config": { - "preferred-install": "dist" + "preferred-install": "dist", + "allow-plugins": { + "composer/installers": true + } }, "minimum-stability": "dev", "prefer-stable": true diff --git a/plugins/rainlab/blog/classes/PostResource.php b/plugins/rainlab/blog/classes/PostResource.php index 9194012..5f421e2 100644 --- a/plugins/rainlab/blog/classes/PostResource.php +++ b/plugins/rainlab/blog/classes/PostResource.php @@ -1,33 +1,37 @@ -get('locale'); - - return [ - 'id' => $this->id, - 'title' => $this->getAttributeTranslated('title', $locale), - 'slug' => $this->getAttributeTranslated('slug', $locale), - 'excerpt' => $this->getAttributeTranslated('excerpt', $locale), - 'published_at' => $this->published_at->format('d.m.Y'), - 'featured_images' => ImageResource::collection($this->featured_images), - 'content_html' => $this->getAttributeTranslated('content_html', $locale), - 'categories' => CategoryResource::collection($this->categories), - 'powerseo_title' => $this->getAttributeTranslated('powerseo_title', $locale), - 'powerseo_description' => $this->getAttributeTranslated('powerseo_description', $locale), - 'powerseo_keywords' => $this->getAttributeTranslated('powerseo_keywords', $locale), - ]; - } -} +get('locale'); + $path = Config::get('app.cdn').Config::get('cms.storage.media.path'); + + return [ + 'id' => $this->id, + 'title' => $this->getAttributeTranslated('title', $locale), + 'slug' => $this->getAttributeTranslated('slug', $locale), + 'excerpt' => $this->getAttributeTranslated('excerpt', $locale), + 'published_at' => $this->published_at->format('d.m.Y'), + 'type' => $this->type, + 'featured_images' => ImageResource::collection($this->featured_images), + 'video' => $path.$this->video, + 'content_html' => $this->getAttributeTranslated('content_html', $locale), + 'categories' => CategoryResource::collection($this->categories), + 'powerseo_title' => $this->getAttributeTranslated('powerseo_title', $locale), + 'powerseo_description' => $this->getAttributeTranslated('powerseo_description', $locale), + 'powerseo_keywords' => $this->getAttributeTranslated('powerseo_keywords', $locale), + ]; + } +} diff --git a/plugins/rainlab/blog/models/Post.php b/plugins/rainlab/blog/models/Post.php index c472ba0..2d6099a 100644 --- a/plugins/rainlab/blog/models/Post.php +++ b/plugins/rainlab/blog/models/Post.php @@ -1,737 +1,746 @@ - 'required', - 'slug' => ['required', 'regex:/^[a-z0-9\/\:_\-\*\[\]\+\?\|]*$/i', 'unique:rainlab_blog_posts'], - 'content' => 'required', - 'powerseo_title' => 'required', - 'powerseo_description' => 'required', - 'powerseo_keywords' => 'required', - 'excerpt' => 'required' - ]; - - /** - * @var array Attributes that support translation, if available. - */ - public $translatable = [ - 'title', - 'content', - 'content_html', - 'excerpt', - 'metadata', - 'powerseo_title', - 'powerseo_description', - 'powerseo_keywords', - ['slug', 'index' => true] - ]; - - /** - * @var array Attributes to be stored as JSON - */ - protected $jsonable = ['metadata']; - - /** - * The attributes that should be mutated to dates. - * @var array - */ - protected $dates = ['published_at']; - - /** - * The attributes on which the post list can be ordered. - * @var array - */ - public static $allowedSortingOptions = [ - 'title asc' => 'rainlab.blog::lang.sorting.title_asc', - 'title desc' => 'rainlab.blog::lang.sorting.title_desc', - 'created_at asc' => 'rainlab.blog::lang.sorting.created_asc', - 'created_at desc' => 'rainlab.blog::lang.sorting.created_desc', - 'updated_at asc' => 'rainlab.blog::lang.sorting.updated_asc', - 'updated_at desc' => 'rainlab.blog::lang.sorting.updated_desc', - 'published_at asc' => 'rainlab.blog::lang.sorting.published_asc', - 'published_at desc' => 'rainlab.blog::lang.sorting.published_desc', - 'random' => 'rainlab.blog::lang.sorting.random' - ]; - - /* - * Relations - */ - public $belongsTo = [ - 'user' => BackendUser::class - ]; - - public $belongsToMany = [ - 'categories' => [ - Category::class, - 'table' => 'rainlab_blog_posts_categories', - 'order' => 'name' - ] - ]; - - public $attachMany = [ - 'featured_images' => [\System\Models\File::class, 'order' => 'sort_order'], - 'content_images' => \System\Models\File::class - ]; - - /** - * @var array The accessors to append to the model's array form. - */ - protected $appends = ['summary', 'has_summary']; - - public $preview = null; - - /** - * Limit visibility of the published-button - * - * @param $fields - * @param null $context - * @return void - */ - public function filterFields($fields, $context = null) - { - if (!isset($fields->published, $fields->published_at)) { - return; - } - - $user = BackendAuth::getUser(); - - if (!$user->hasAnyAccess(['rainlab.blog.access_publish'])) { - $fields->published->hidden = true; - $fields->published_at->hidden = true; - } - else { - $fields->published->hidden = false; - $fields->published_at->hidden = false; - } - } - - /** - * beforeValidate - */ - public function beforeValidate() - { - if (empty($this->user)) { - $user = BackendAuth::getUser(); - if (!is_null($user)) { - $this->user = $user->id; - } - } - - $this->content_html = self::formatHtml($this->content); - } - - /** - * afterValidate - */ - public function afterValidate() - { - if ($this->published && !$this->published_at) { - throw new ValidationException([ - 'published_at' => Lang::get('rainlab.blog::lang.post.published_validation') - ]); - } - } - - /** - * getUserOptions - */ - public function getUserOptions() - { - $options = []; - - foreach (BackendUser::all() as $user) { - $options[$user->id] = $user->fullname . ' ('.$user->login.')'; - } - - return $options; - } - - /** - * Sets the "url" attribute with a URL to this object. - * @param string $pageName - * @param Controller $controller - * @param array $params Override request URL parameters - * - * @return string - */ - public function setUrl($pageName, $controller, $params = []) - { - $params = array_merge([ - 'id' => $this->id, - 'slug' => $this->slug, - ], $params); - - if (empty($params['category'])) { - $params['category'] = $this->categories->count() ? $this->categories->first()->slug : null; - } - - // Expose published year, month and day as URL parameters. - if ($this->published) { - $params['year'] = $this->published_at->format('Y'); - $params['month'] = $this->published_at->format('m'); - $params['day'] = $this->published_at->format('d'); - } - - return $this->url = $controller->pageUrl($pageName, $params); - } - - /** - * Used to test if a certain user has permission to edit post, - * returns TRUE if the user is the owner or has other posts access. - * @param BackendUser $user - * @return bool - */ - public function canEdit(BackendUser $user) - { - return ($this->user_id == $user->id) || $user->hasAnyAccess(['rainlab.blog.access_other_posts']); - } - - public static function formatHtml($input, $preview = false) - { - $result = Markdown::parse(trim($input)); - - // Check to see if the HTML should be cleaned from potential XSS - $user = BackendAuth::getUser(); - if (!$user || !$user->hasAccess('backend.allow_unsafe_markdown')) { - $result = Html::clean($result); - } - - if ($preview) { - $result = str_replace('
', '
', $result);
-        }
-
-        $result = TagProcessor::instance()->processTags($result, $preview);
-
-        return $result;
-    }
-
-    //
-    // Scopes
-    //
-
-    public function scopeIsPublished($query)
-    {
-        return $query
-            ->whereNotNull('published')
-            ->where('published', true)
-            ->whereNotNull('published_at')
-            ->where('published_at', '<', Carbon::now())
-        ;
-    }
-
-    /**
-     * Lists posts for the frontend
-     *
-     * @param        $query
-     * @param  array $options Display options
-     * @return Post
-     */
-    public function scopeListFrontEnd($query, $options)
-    {
-        /*
-         * Default options
-         */
-        extract(array_merge([
-            'page'             => 1,
-            'perPage'          => 30,
-            'sort'             => 'created_at',
-            'categories'       => null,
-            'exceptCategories' => null,
-            'category'         => null,
-            'search'           => '',
-            'published'        => true,
-            'featured'         => null,
-            'date'             => '',
-            'exceptPost'       => null
-        ], $options));
-
-        $searchableFields = ['title', 'slug', 'excerpt', 'content'];
-
-        if ($published) {
-            $query->isPublished();
-        }
-
-
-        if (isset($featured)) {
-            $query->where('featured', $featured);
-        }
-
-        $date = trim($date);
-
-        if (strtotime($date)) {
-            $query->whereDate('published_at', '=', $date);
-        }
-
-        /*
-         * Except post(s)
-         */
-        if ($exceptPost) {
-            $exceptPosts = (is_array($exceptPost)) ? $exceptPost : [$exceptPost];
-            $exceptPostIds = [];
-            $exceptPostSlugs = [];
-
-            foreach ($exceptPosts as $exceptPost) {
-                $exceptPost = trim($exceptPost);
-
-                if (is_numeric($exceptPost)) {
-                    $exceptPostIds[] = $exceptPost;
-                } else {
-                    $exceptPostSlugs[] = $exceptPost;
-                }
-            }
-
-            if (count($exceptPostIds)) {
-                $query->whereNotIn('id', $exceptPostIds);
-            }
-            if (count($exceptPostSlugs)) {
-                $query->whereNotIn('slug', $exceptPostSlugs);
-            }
-        }
-
-        /*
-         * Sorting
-         */
-        if (in_array($sort, array_keys(static::$allowedSortingOptions))) {
-            if ($sort == 'random') {
-                $query->inRandomOrder();
-            } else {
-                @list($sortField, $sortDirection) = explode(' ', $sort);
-
-                if (is_null($sortDirection)) {
-                    $sortDirection = "desc";
-                }
-
-                $query->orderBy($sortField, $sortDirection);
-            }
-        }
-
-        /*
-         * Search
-         */
-        $search = trim($search);
-        if (strlen($search)) {
-            $query->searchWhere($search, $searchableFields);
-        }
-
-        /*
-         * Categories
-         */
-        if ($categories !== null) {
-            $categories = is_array($categories) ? $categories : [$categories];
-            $query->whereHas('categories', function($q) use ($categories) {
-                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
-            });
-        }
-
-        /*
-         * Except Categories
-         */
-        if (!empty($exceptCategories)) {
-            $exceptCategories = is_array($exceptCategories) ? $exceptCategories : [$exceptCategories];
-            array_walk($exceptCategories, 'trim');
-
-            $query->whereDoesntHave('categories', function ($q) use ($exceptCategories) {
-                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('slug', $exceptCategories);
-            });
-        }
-
-        /*
-         * Category, including children
-         */
-        if ($category !== null) {
-            $category = Category::find($category);
-
-            $categories = $category->getAllChildrenAndSelf()->lists('id');
-            $query->whereHas('categories', function($q) use ($categories) {
-                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
-            });
-        }
-
-        return $query->paginate($perPage, $page);
-    }
-
-    /**
-     * Allows filtering for specifc categories.
-     * @param  Illuminate\Query\Builder  $query      QueryBuilder
-     * @param  array                     $categories List of category ids
-     * @return Illuminate\Query\Builder              QueryBuilder
-     */
-    public function scopeFilterCategories($query, $categories)
-    {
-        return $query->whereHas('categories', function($q) use ($categories) {
-            $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
-        });
-    }
-
-    //
-    // Summary / Excerpt
-    //
-
-    /**
-     * Used by "has_summary", returns true if this post uses a summary (more tag).
-     * @return boolean
-     */
-    public function getHasSummaryAttribute()
-    {
-        $more = Config::get('rainlab.blog::summary_separator', '');
-        $length = Config::get('rainlab.blog::summary_default_length', 600);
-
-        return (
-            !!strlen(trim($this->excerpt)) ||
-            strpos($this->content_html, $more) !== false ||
-            strlen(Html::strip($this->content_html)) > $length
-        );
-    }
-
-    /**
-     * Used by "summary", if no excerpt is provided, generate one from the content.
-     * Returns the HTML content before the  tag or a limited 600
-     * character version.
-     *
-     * @return string
-     */
-    public function getSummaryAttribute()
-    {
-        $excerpt = $this->excerpt;
-        if (strlen(trim($excerpt))) {
-            return $excerpt;
-        }
-
-        $more = Config::get('rainlab.blog::summary_separator', '');
-
-        if (strpos($this->content_html, $more) !== false) {
-            $parts = explode($more, $this->content_html);
-
-            return array_get($parts, 0);
-        }
-
-        $length = Config::get('rainlab.blog::summary_default_length', 600);
-
-        return Html::limit($this->content_html, $length);
-    }
-
-    //
-    // Next / Previous
-    //
-
-    /**
-     * Apply a constraint to the query to find the nearest sibling
-     *
-     *     // Get the next post
-     *     Post::applySibling()->first();
-     *
-     *     // Get the previous post
-     *     Post::applySibling(-1)->first();
-     *
-     *     // Get the previous post, ordered by the ID attribute instead
-     *     Post::applySibling(['direction' => -1, 'attribute' => 'id'])->first();
-     *
-     * @param       $query
-     * @param array $options
-     * @return
-     */
-    public function scopeApplySibling($query, $options = [])
-    {
-        if (!is_array($options)) {
-            $options = ['direction' => $options];
-        }
-
-        extract(array_merge([
-            'direction' => 'next',
-            'attribute' => 'published_at'
-        ], $options));
-
-        $isPrevious = in_array($direction, ['previous', -1]);
-        $directionOrder = $isPrevious ? 'asc' : 'desc';
-        $directionOperator = $isPrevious ? '>' : '<';
-
-        $query->where('id', '<>', $this->id);
-
-        if (!is_null($this->$attribute)) {
-            $query->where($attribute, $directionOperator, $this->$attribute);
-        }
-
-        return $query->orderBy($attribute, $directionOrder);
-    }
-
-    /**
-     * Returns the next post, if available.
-     * @return self
-     */
-    public function nextPost()
-    {
-        return self::isPublished()->applySibling()->first();
-    }
-
-    /**
-     * Returns the previous post, if available.
-     * @return self
-     */
-    public function previousPost()
-    {
-        return self::isPublished()->applySibling(-1)->first();
-    }
-
-    //
-    // Menu helpers
-    //
-
-    /**
-     * Handler for the pages.menuitem.getTypeInfo event.
-     * Returns a menu item type information. The type information is returned as array
-     * with the following elements:
-     * - references - a list of the item type reference options. The options are returned in the
-     *   ["key"] => "title" format for options that don't have sub-options, and in the format
-     *   ["key"] => ["title"=>"Option title", "items"=>[...]] for options that have sub-options. Optional,
-     *   required only if the menu item type requires references.
-     * - nesting - Boolean value indicating whether the item type supports nested items. Optional,
-     *   false if omitted.
-     * - dynamicItems - Boolean value indicating whether the item type could generate new menu items.
-     *   Optional, false if omitted.
-     * - cmsPages - a list of CMS pages (objects of the Cms\Classes\Page class), if the item type requires a CMS page reference to
-     *   resolve the item URL.
-     *
-     * @param string $type Specifies the menu item type
-     * @return array Returns an array
-     */
-    public static function getMenuTypeInfo($type)
-    {
-        $result = [];
-
-        if ($type == 'blog-post') {
-            $references = [];
-
-            $posts = self::orderBy('title')->get();
-            foreach ($posts as $post) {
-                $references[$post->id] = $post->title;
-            }
-
-            $result = [
-                'references'   => $references,
-                'nesting'      => false,
-                'dynamicItems' => false
-            ];
-        }
-
-        if ($type == 'all-blog-posts') {
-            $result = [
-                'dynamicItems' => true
-            ];
-        }
-
-        if ($type == 'category-blog-posts') {
-            $references = [];
-
-            $categories = Category::orderBy('name')->get();
-            foreach ($categories as $category) {
-                $references[$category->id] = $category->name;
-            }
-
-            $result = [
-                'references'   => $references,
-                'dynamicItems' => true
-            ];
-        }
-
-        if ($result) {
-            $theme = Theme::getActiveTheme();
-
-            $pages = CmsPage::listInTheme($theme, true);
-            $cmsPages = [];
-
-            foreach ($pages as $page) {
-                if (!$page->hasComponent('blogPost')) {
-                    continue;
-                }
-
-                /*
-                 * Component must use a categoryPage filter with a routing parameter and post slug
-                 * eg: categoryPage = "{{ :somevalue }}", slug = "{{ :somevalue }}"
-                 */
-                $properties = $page->getComponentProperties('blogPost');
-                if (!isset($properties['categoryPage']) || !preg_match('/{{\s*:/', $properties['slug'])) {
-                    continue;
-                }
-
-                $cmsPages[] = $page;
-            }
-
-            $result['cmsPages'] = $cmsPages;
-        }
-
-        return $result;
-    }
-
-    /**
-     * Handler for the pages.menuitem.resolveItem event.
-     * Returns information about a menu item. The result is an array
-     * with the following keys:
-     * - url - the menu item URL. Not required for menu item types that return all available records.
-     *   The URL should be returned relative to the website root and include the subdirectory, if any.
-     *   Use the Url::to() helper to generate the URLs.
-     * - isActive - determines whether the menu item is active. Not required for menu item types that
-     *   return all available records.
-     * - items - an array of arrays with the same keys (url, isActive, items) + the title key.
-     *   The items array should be added only if the $item's $nesting property value is TRUE.
-     *
-     * @param \RainLab\Pages\Classes\MenuItem $item Specifies the menu item.
-     * @param \Cms\Classes\Theme $theme Specifies the current theme.
-     * @param string $url Specifies the current page URL, normalized, in lower case
-     * The URL is specified relative to the website root, it includes the subdirectory name, if any.
-     * @return mixed Returns an array. Returns null if the item cannot be resolved.
-     */
-    public static function resolveMenuItem($item, $url, $theme)
-    {
-        $result = null;
-
-        if ($item->type == 'blog-post') {
-            if (!$item->reference || !$item->cmsPage) {
-                return;
-            }
-
-            $category = self::find($item->reference);
-            if (!$category) {
-                return;
-            }
-
-            $pageUrl = self::getPostPageUrl($item->cmsPage, $category, $theme);
-            if (!$pageUrl) {
-                return;
-            }
-
-            $pageUrl = Url::to($pageUrl);
-
-            $result = [];
-            $result['url'] = $pageUrl;
-            $result['isActive'] = $pageUrl == $url;
-            $result['mtime'] = $category->updated_at;
-        }
-        elseif ($item->type == 'all-blog-posts') {
-            $result = [
-                'items' => []
-            ];
-
-            $posts = self::isPublished()
-                ->orderBy('title')
-                ->get()
-            ;
-
-            foreach ($posts as $post) {
-                $postItem = [
-                    'title' => $post->title,
-                    'url'   => self::getPostPageUrl($item->cmsPage, $post, $theme),
-                    'mtime' => $post->updated_at
-                ];
-
-                $postItem['isActive'] = $postItem['url'] == $url;
-
-                $result['items'][] = $postItem;
-            }
-        }
-        elseif ($item->type == 'category-blog-posts') {
-            if (!$item->reference || !$item->cmsPage) {
-                return;
-            }
-
-            $category = Category::find($item->reference);
-            if (!$category) {
-                return;
-            }
-
-            $result = [
-                'items' => []
-            ];
-
-            $query = self::isPublished()
-            ->orderBy('title');
-
-            $categories = $category->getAllChildrenAndSelf()->lists('id');
-            $query->whereHas('categories', function($q) use ($categories) {
-                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
-            });
-
-            $posts = $query->get();
-
-            foreach ($posts as $post) {
-                $postItem = [
-                    'title' => $post->title,
-                    'url'   => self::getPostPageUrl($item->cmsPage, $post, $theme),
-                    'mtime' => $post->updated_at
-                ];
-
-                $postItem['isActive'] = $postItem['url'] == $url;
-
-                $result['items'][] = $postItem;
-            }
-        }
-
-        return $result;
-    }
-
-    /**
-     * Returns URL of a post page.
-     *
-     * @param $pageCode
-     * @param $category
-     * @param $theme
-     */
-    protected static function getPostPageUrl($pageCode, $category, $theme)
-    {
-        $page = CmsPage::loadCached($theme, $pageCode);
-        if (!$page) {
-            return;
-        }
-
-        $properties = $page->getComponentProperties('blogPost');
-        if (!isset($properties['slug'])) {
-            return;
-        }
-
-        /*
-         * Extract the routing parameter name from the category filter
-         * eg: {{ :someRouteParam }}
-         */
-        if (!preg_match('/^\{\{([^\}]+)\}\}$/', $properties['slug'], $matches)) {
-            return;
-        }
-
-        $paramName = substr(trim($matches[1]), 1);
-        $params = [
-            $paramName => $category->slug,
-            'year'  => $category->published_at->format('Y'),
-            'month' => $category->published_at->format('m'),
-            'day'   => $category->published_at->format('d')
-        ];
-        $url = CmsPage::url($page->getBaseFileName(), $params);
-
-        return $url;
-    }
-}
+ 'required',
+        'slug'    => ['required', 'regex:/^[a-z0-9\/\:_\-\*\[\]\+\?\|]*$/i', 'unique:rainlab_blog_posts'],
+        'content' => 'required',
+        'powerseo_title' => 'required',
+        'powerseo_description' => 'required',
+        'powerseo_keywords' => 'required',
+        'excerpt' => 'required'
+    ];
+
+    /**
+     * @var array Attributes that support translation, if available.
+     */
+    public $translatable = [
+        'title',
+        'content',
+        'content_html',
+        'excerpt',
+        'metadata',
+        'powerseo_title',
+        'powerseo_description',
+        'powerseo_keywords',
+        ['slug', 'index' => true]
+    ];
+
+    /**
+     * @var array Attributes to be stored as JSON
+     */
+    protected $jsonable = ['metadata'];
+
+    /**
+     * The attributes that should be mutated to dates.
+     * @var array
+     */
+    protected $dates = ['published_at'];
+
+    /**
+     * The attributes on which the post list can be ordered.
+     * @var array
+     */
+    public static $allowedSortingOptions = [
+        'title asc'         => 'rainlab.blog::lang.sorting.title_asc',
+        'title desc'        => 'rainlab.blog::lang.sorting.title_desc',
+        'created_at asc'    => 'rainlab.blog::lang.sorting.created_asc',
+        'created_at desc'   => 'rainlab.blog::lang.sorting.created_desc',
+        'updated_at asc'    => 'rainlab.blog::lang.sorting.updated_asc',
+        'updated_at desc'   => 'rainlab.blog::lang.sorting.updated_desc',
+        'published_at asc'  => 'rainlab.blog::lang.sorting.published_asc',
+        'published_at desc' => 'rainlab.blog::lang.sorting.published_desc',
+        'random'            => 'rainlab.blog::lang.sorting.random'
+    ];
+
+    /*
+     * Relations
+     */
+    public $belongsTo = [
+        'user' => BackendUser::class
+    ];
+
+    public $belongsToMany = [
+        'categories' => [
+            Category::class,
+            'table' => 'rainlab_blog_posts_categories',
+            'order' => 'name'
+        ]
+    ];
+
+    public $attachMany = [
+        'featured_images' => [\System\Models\File::class, 'order' => 'sort_order'],
+        'content_images'  => \System\Models\File::class
+    ];
+
+    /**
+     * @var array The accessors to append to the model's array form.
+     */
+    protected $appends = ['summary', 'has_summary'];
+
+    public $preview = null;
+
+    /**
+     * Limit visibility of the published-button
+     *
+     * @param       $fields
+     * @param  null $context
+     * @return void
+     */
+    public function filterFields($fields, $context = null)
+    {
+        if(isset($fields->type)){
+       // dd($fields->type->value);
+            if($fields->type->value == 'photo'){
+                $fields->video->hidden = true;
+            }else{
+                $fields->featured_images->hidden = true;
+            }
+        }
+        
+        if (!isset($fields->published, $fields->published_at)) {
+            return;
+        }
+
+        $user = BackendAuth::getUser();
+
+        if (!$user->hasAnyAccess(['rainlab.blog.access_publish'])) {
+            $fields->published->hidden = true;
+            $fields->published_at->hidden = true;
+        }
+        else {
+            $fields->published->hidden = false;
+            $fields->published_at->hidden = false;
+        }
+    }
+
+    /**
+     * beforeValidate
+     */
+    public function beforeValidate()
+    {
+        if (empty($this->user)) {
+            $user = BackendAuth::getUser();
+            if (!is_null($user)) {
+                $this->user = $user->id;
+            }
+        }
+
+        $this->content_html = self::formatHtml($this->content);
+    }
+
+    /**
+     * afterValidate
+     */
+    public function afterValidate()
+    {
+        if ($this->published && !$this->published_at) {
+            throw new ValidationException([
+               'published_at' => Lang::get('rainlab.blog::lang.post.published_validation')
+            ]);
+        }
+    }
+
+    /**
+     * getUserOptions
+     */
+    public function getUserOptions()
+    {
+        $options = [];
+
+        foreach (BackendUser::all() as $user) {
+            $options[$user->id] = $user->fullname . ' ('.$user->login.')';
+        }
+
+        return $options;
+    }
+
+    /**
+     * Sets the "url" attribute with a URL to this object.
+     * @param string $pageName
+     * @param Controller $controller
+     * @param array $params Override request URL parameters
+     *
+     * @return string
+     */
+    public function setUrl($pageName, $controller, $params = [])
+    {
+        $params = array_merge([
+            'id'   => $this->id,
+            'slug' => $this->slug,
+        ], $params);
+
+        if (empty($params['category'])) {
+            $params['category'] = $this->categories->count() ? $this->categories->first()->slug : null;
+        }
+
+        // Expose published year, month and day as URL parameters.
+        if ($this->published) {
+            $params['year']  = $this->published_at->format('Y');
+            $params['month'] = $this->published_at->format('m');
+            $params['day']   = $this->published_at->format('d');
+        }
+
+        return $this->url = $controller->pageUrl($pageName, $params);
+    }
+
+    /**
+     * Used to test if a certain user has permission to edit post,
+     * returns TRUE if the user is the owner or has other posts access.
+     * @param  BackendUser $user
+     * @return bool
+     */
+    public function canEdit(BackendUser $user)
+    {
+        return ($this->user_id == $user->id) || $user->hasAnyAccess(['rainlab.blog.access_other_posts']);
+    }
+
+    public static function formatHtml($input, $preview = false)
+    {
+        $result = Markdown::parse(trim($input));
+
+        // Check to see if the HTML should be cleaned from potential XSS
+        $user = BackendAuth::getUser();
+        if (!$user || !$user->hasAccess('backend.allow_unsafe_markdown')) {
+            $result = Html::clean($result);
+        }
+
+        if ($preview) {
+            $result = str_replace('
', '
', $result);
+        }
+
+        $result = TagProcessor::instance()->processTags($result, $preview);
+
+        return $result;
+    }
+
+    //
+    // Scopes
+    //
+
+    public function scopeIsPublished($query)
+    {
+        return $query
+            ->whereNotNull('published')
+            ->where('published', true)
+            ->whereNotNull('published_at')
+            ->where('published_at', '<', Carbon::now())
+        ;
+    }
+
+    /**
+     * Lists posts for the frontend
+     *
+     * @param        $query
+     * @param  array $options Display options
+     * @return Post
+     */
+    public function scopeListFrontEnd($query, $options)
+    {
+        /*
+         * Default options
+         */
+        extract(array_merge([
+            'page'             => 1,
+            'perPage'          => 30,
+            'sort'             => 'created_at',
+            'categories'       => null,
+            'exceptCategories' => null,
+            'category'         => null,
+            'search'           => '',
+            'published'        => true,
+            'featured'         => null,
+            'date'             => '',
+            'exceptPost'       => null
+        ], $options));
+
+        $searchableFields = ['title', 'slug', 'excerpt', 'content'];
+
+        if ($published) {
+            $query->isPublished();
+        }
+
+
+        if (isset($featured)) {
+            $query->where('featured', $featured);
+        }
+
+        $date = trim($date);
+
+        if (strtotime($date)) {
+            $query->whereDate('published_at', '=', $date);
+        }
+
+        /*
+         * Except post(s)
+         */
+        if ($exceptPost) {
+            $exceptPosts = (is_array($exceptPost)) ? $exceptPost : [$exceptPost];
+            $exceptPostIds = [];
+            $exceptPostSlugs = [];
+
+            foreach ($exceptPosts as $exceptPost) {
+                $exceptPost = trim($exceptPost);
+
+                if (is_numeric($exceptPost)) {
+                    $exceptPostIds[] = $exceptPost;
+                } else {
+                    $exceptPostSlugs[] = $exceptPost;
+                }
+            }
+
+            if (count($exceptPostIds)) {
+                $query->whereNotIn('id', $exceptPostIds);
+            }
+            if (count($exceptPostSlugs)) {
+                $query->whereNotIn('slug', $exceptPostSlugs);
+            }
+        }
+
+        /*
+         * Sorting
+         */
+        if (in_array($sort, array_keys(static::$allowedSortingOptions))) {
+            if ($sort == 'random') {
+                $query->inRandomOrder();
+            } else {
+                @list($sortField, $sortDirection) = explode(' ', $sort);
+
+                if (is_null($sortDirection)) {
+                    $sortDirection = "desc";
+                }
+
+                $query->orderBy($sortField, $sortDirection);
+            }
+        }
+
+        /*
+         * Search
+         */
+        $search = trim($search);
+        if (strlen($search)) {
+            $query->searchWhere($search, $searchableFields);
+        }
+
+        /*
+         * Categories
+         */
+        if ($categories !== null) {
+            $categories = is_array($categories) ? $categories : [$categories];
+            $query->whereHas('categories', function($q) use ($categories) {
+                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
+            });
+        }
+
+        /*
+         * Except Categories
+         */
+        if (!empty($exceptCategories)) {
+            $exceptCategories = is_array($exceptCategories) ? $exceptCategories : [$exceptCategories];
+            array_walk($exceptCategories, 'trim');
+
+            $query->whereDoesntHave('categories', function ($q) use ($exceptCategories) {
+                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('slug', $exceptCategories);
+            });
+        }
+
+        /*
+         * Category, including children
+         */
+        if ($category !== null) {
+            $category = Category::find($category);
+
+            $categories = $category->getAllChildrenAndSelf()->lists('id');
+            $query->whereHas('categories', function($q) use ($categories) {
+                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
+            });
+        }
+
+        return $query->paginate($perPage, $page);
+    }
+
+    /**
+     * Allows filtering for specifc categories.
+     * @param  Illuminate\Query\Builder  $query      QueryBuilder
+     * @param  array                     $categories List of category ids
+     * @return Illuminate\Query\Builder              QueryBuilder
+     */
+    public function scopeFilterCategories($query, $categories)
+    {
+        return $query->whereHas('categories', function($q) use ($categories) {
+            $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
+        });
+    }
+
+    //
+    // Summary / Excerpt
+    //
+
+    /**
+     * Used by "has_summary", returns true if this post uses a summary (more tag).
+     * @return boolean
+     */
+    public function getHasSummaryAttribute()
+    {
+        $more = Config::get('rainlab.blog::summary_separator', '');
+        $length = Config::get('rainlab.blog::summary_default_length', 600);
+
+        return (
+            !!strlen(trim($this->excerpt)) ||
+            strpos($this->content_html, $more) !== false ||
+            strlen(Html::strip($this->content_html)) > $length
+        );
+    }
+
+    /**
+     * Used by "summary", if no excerpt is provided, generate one from the content.
+     * Returns the HTML content before the  tag or a limited 600
+     * character version.
+     *
+     * @return string
+     */
+    public function getSummaryAttribute()
+    {
+        $excerpt = $this->excerpt;
+        if (strlen(trim($excerpt))) {
+            return $excerpt;
+        }
+
+        $more = Config::get('rainlab.blog::summary_separator', '');
+
+        if (strpos($this->content_html, $more) !== false) {
+            $parts = explode($more, $this->content_html);
+
+            return array_get($parts, 0);
+        }
+
+        $length = Config::get('rainlab.blog::summary_default_length', 600);
+
+        return Html::limit($this->content_html, $length);
+    }
+
+    //
+    // Next / Previous
+    //
+
+    /**
+     * Apply a constraint to the query to find the nearest sibling
+     *
+     *     // Get the next post
+     *     Post::applySibling()->first();
+     *
+     *     // Get the previous post
+     *     Post::applySibling(-1)->first();
+     *
+     *     // Get the previous post, ordered by the ID attribute instead
+     *     Post::applySibling(['direction' => -1, 'attribute' => 'id'])->first();
+     *
+     * @param       $query
+     * @param array $options
+     * @return
+     */
+    public function scopeApplySibling($query, $options = [])
+    {
+        if (!is_array($options)) {
+            $options = ['direction' => $options];
+        }
+
+        extract(array_merge([
+            'direction' => 'next',
+            'attribute' => 'published_at'
+        ], $options));
+
+        $isPrevious = in_array($direction, ['previous', -1]);
+        $directionOrder = $isPrevious ? 'asc' : 'desc';
+        $directionOperator = $isPrevious ? '>' : '<';
+
+        $query->where('id', '<>', $this->id);
+
+        if (!is_null($this->$attribute)) {
+            $query->where($attribute, $directionOperator, $this->$attribute);
+        }
+
+        return $query->orderBy($attribute, $directionOrder);
+    }
+
+    /**
+     * Returns the next post, if available.
+     * @return self
+     */
+    public function nextPost()
+    {
+        return self::isPublished()->applySibling()->first();
+    }
+
+    /**
+     * Returns the previous post, if available.
+     * @return self
+     */
+    public function previousPost()
+    {
+        return self::isPublished()->applySibling(-1)->first();
+    }
+
+    //
+    // Menu helpers
+    //
+
+    /**
+     * Handler for the pages.menuitem.getTypeInfo event.
+     * Returns a menu item type information. The type information is returned as array
+     * with the following elements:
+     * - references - a list of the item type reference options. The options are returned in the
+     *   ["key"] => "title" format for options that don't have sub-options, and in the format
+     *   ["key"] => ["title"=>"Option title", "items"=>[...]] for options that have sub-options. Optional,
+     *   required only if the menu item type requires references.
+     * - nesting - Boolean value indicating whether the item type supports nested items. Optional,
+     *   false if omitted.
+     * - dynamicItems - Boolean value indicating whether the item type could generate new menu items.
+     *   Optional, false if omitted.
+     * - cmsPages - a list of CMS pages (objects of the Cms\Classes\Page class), if the item type requires a CMS page reference to
+     *   resolve the item URL.
+     *
+     * @param string $type Specifies the menu item type
+     * @return array Returns an array
+     */
+    public static function getMenuTypeInfo($type)
+    {
+        $result = [];
+
+        if ($type == 'blog-post') {
+            $references = [];
+
+            $posts = self::orderBy('title')->get();
+            foreach ($posts as $post) {
+                $references[$post->id] = $post->title;
+            }
+
+            $result = [
+                'references'   => $references,
+                'nesting'      => false,
+                'dynamicItems' => false
+            ];
+        }
+
+        if ($type == 'all-blog-posts') {
+            $result = [
+                'dynamicItems' => true
+            ];
+        }
+
+        if ($type == 'category-blog-posts') {
+            $references = [];
+
+            $categories = Category::orderBy('name')->get();
+            foreach ($categories as $category) {
+                $references[$category->id] = $category->name;
+            }
+
+            $result = [
+                'references'   => $references,
+                'dynamicItems' => true
+            ];
+        }
+
+        if ($result) {
+            $theme = Theme::getActiveTheme();
+
+            $pages = CmsPage::listInTheme($theme, true);
+            $cmsPages = [];
+
+            foreach ($pages as $page) {
+                if (!$page->hasComponent('blogPost')) {
+                    continue;
+                }
+
+                /*
+                 * Component must use a categoryPage filter with a routing parameter and post slug
+                 * eg: categoryPage = "{{ :somevalue }}", slug = "{{ :somevalue }}"
+                 */
+                $properties = $page->getComponentProperties('blogPost');
+                if (!isset($properties['categoryPage']) || !preg_match('/{{\s*:/', $properties['slug'])) {
+                    continue;
+                }
+
+                $cmsPages[] = $page;
+            }
+
+            $result['cmsPages'] = $cmsPages;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Handler for the pages.menuitem.resolveItem event.
+     * Returns information about a menu item. The result is an array
+     * with the following keys:
+     * - url - the menu item URL. Not required for menu item types that return all available records.
+     *   The URL should be returned relative to the website root and include the subdirectory, if any.
+     *   Use the Url::to() helper to generate the URLs.
+     * - isActive - determines whether the menu item is active. Not required for menu item types that
+     *   return all available records.
+     * - items - an array of arrays with the same keys (url, isActive, items) + the title key.
+     *   The items array should be added only if the $item's $nesting property value is TRUE.
+     *
+     * @param \RainLab\Pages\Classes\MenuItem $item Specifies the menu item.
+     * @param \Cms\Classes\Theme $theme Specifies the current theme.
+     * @param string $url Specifies the current page URL, normalized, in lower case
+     * The URL is specified relative to the website root, it includes the subdirectory name, if any.
+     * @return mixed Returns an array. Returns null if the item cannot be resolved.
+     */
+    public static function resolveMenuItem($item, $url, $theme)
+    {
+        $result = null;
+
+        if ($item->type == 'blog-post') {
+            if (!$item->reference || !$item->cmsPage) {
+                return;
+            }
+
+            $category = self::find($item->reference);
+            if (!$category) {
+                return;
+            }
+
+            $pageUrl = self::getPostPageUrl($item->cmsPage, $category, $theme);
+            if (!$pageUrl) {
+                return;
+            }
+
+            $pageUrl = Url::to($pageUrl);
+
+            $result = [];
+            $result['url'] = $pageUrl;
+            $result['isActive'] = $pageUrl == $url;
+            $result['mtime'] = $category->updated_at;
+        }
+        elseif ($item->type == 'all-blog-posts') {
+            $result = [
+                'items' => []
+            ];
+
+            $posts = self::isPublished()
+                ->orderBy('title')
+                ->get()
+            ;
+
+            foreach ($posts as $post) {
+                $postItem = [
+                    'title' => $post->title,
+                    'url'   => self::getPostPageUrl($item->cmsPage, $post, $theme),
+                    'mtime' => $post->updated_at
+                ];
+
+                $postItem['isActive'] = $postItem['url'] == $url;
+
+                $result['items'][] = $postItem;
+            }
+        }
+        elseif ($item->type == 'category-blog-posts') {
+            if (!$item->reference || !$item->cmsPage) {
+                return;
+            }
+
+            $category = Category::find($item->reference);
+            if (!$category) {
+                return;
+            }
+
+            $result = [
+                'items' => []
+            ];
+
+            $query = self::isPublished()
+            ->orderBy('title');
+
+            $categories = $category->getAllChildrenAndSelf()->lists('id');
+            $query->whereHas('categories', function($q) use ($categories) {
+                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
+            });
+
+            $posts = $query->get();
+
+            foreach ($posts as $post) {
+                $postItem = [
+                    'title' => $post->title,
+                    'url'   => self::getPostPageUrl($item->cmsPage, $post, $theme),
+                    'mtime' => $post->updated_at
+                ];
+
+                $postItem['isActive'] = $postItem['url'] == $url;
+
+                $result['items'][] = $postItem;
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Returns URL of a post page.
+     *
+     * @param $pageCode
+     * @param $category
+     * @param $theme
+     */
+    protected static function getPostPageUrl($pageCode, $category, $theme)
+    {
+        $page = CmsPage::loadCached($theme, $pageCode);
+        if (!$page) {
+            return;
+        }
+
+        $properties = $page->getComponentProperties('blogPost');
+        if (!isset($properties['slug'])) {
+            return;
+        }
+
+        /*
+         * Extract the routing parameter name from the category filter
+         * eg: {{ :someRouteParam }}
+         */
+        if (!preg_match('/^\{\{([^\}]+)\}\}$/', $properties['slug'], $matches)) {
+            return;
+        }
+
+        $paramName = substr(trim($matches[1]), 1);
+        $params = [
+            $paramName => $category->slug,
+            'year'  => $category->published_at->format('Y'),
+            'month' => $category->published_at->format('m'),
+            'day'   => $category->published_at->format('d')
+        ];
+        $url = CmsPage::url($page->getBaseFileName(), $params);
+
+        return $url;
+    }
+}
diff --git a/plugins/rainlab/blog/models/post/fields.yaml b/plugins/rainlab/blog/models/post/fields.yaml
index 29304d3..1ed19d1 100644
--- a/plugins/rainlab/blog/models/post/fields.yaml
+++ b/plugins/rainlab/blog/models/post/fields.yaml
@@ -1,73 +1,95 @@
-fields:
-    title:
-        label: 'rainlab.blog::lang.post.title'
-        span: left
-        placeholder: 'rainlab.blog::lang.post.title_placeholder'
-        type: text
-    slug:
-        label: 'rainlab.blog::lang.post.slug'
-        span: right
-        placeholder: 'rainlab.blog::lang.post.slug_placeholder'
-        preset:
-            field: title
-            type: slug
-        type: text
-    toolbar:
-        type: partial
-        path: post_toolbar
-        cssClass: collapse-visible
-secondaryTabs:
-    fields:
-        content:
-            tab: 'rainlab.blog::lang.post.tab_edit'
-            type: RainLab\Blog\FormWidgets\BlogMarkdown
-            cssClass: 'field-slim blog-post-preview'
-            stretch: true
-            mode: split
-        categories:
-            nameFrom: name
-            descriptionFrom: description
-            span: left
-            type: relation
-            commentAbove: 'rainlab.blog::lang.post.categories_comment'
-            tab: 'rainlab.blog::lang.post.tab_categories'
-        published:
-            label: 'rainlab.blog::lang.post.published'
-            span: left
-            type: checkbox
-            tab: 'rainlab.blog::lang.post.tab_manage'
-        featured:
-            label: 'показать в  слайдере на главной странице'
-            span: auto
-            type: checkbox
-            comment: 'не отмечать  если не нужно выставлять на слайдере'
-            tab: 'rainlab.blog::lang.post.tab_manage'
-        user:
-            tab: 'rainlab.blog::lang.post.tab_manage'
-            label: 'rainlab.blog::lang.post.published_by'
-            span: right
-            type: dropdown
-            emptyOption: 'rainlab.blog::lang.post.current_user'
-        published_at:
-            tab: 'rainlab.blog::lang.post.tab_manage'
-            label: 'rainlab.blog::lang.post.published_on'
-            span: left
-            cssClass: checkbox-align
-            type: datepicker
-            mode: datetime
-            trigger:
-                action: enable
-                field: published
-                condition: checked
-        excerpt:
-            tab: 'rainlab.blog::lang.post.tab_manage'
-            label: 'rainlab.blog::lang.post.excerpt'
-            type: textarea
-            size: small
-        featured_images:
-            tab: 'rainlab.blog::lang.post.tab_manage'
-            label: 'rainlab.blog::lang.post.featured_images'
-            type: fileupload
-            mode: image
-            imageWidth: 200
-            imageHeight: 200
+fields:
+    title:
+        label: 'rainlab.blog::lang.post.title'
+        span: left
+        placeholder: 'rainlab.blog::lang.post.title_placeholder'
+        type: text
+    slug:
+        label: 'rainlab.blog::lang.post.slug'
+        span: right
+        placeholder: 'rainlab.blog::lang.post.slug_placeholder'
+        preset:
+            field: title
+            type: slug
+        type: text
+    toolbar:
+        type: partial
+        path: post_toolbar
+        cssClass: collapse-visible
+secondaryTabs:
+    fields:
+        content:
+            tab: 'rainlab.blog::lang.post.tab_edit'
+            type: RainLab\Blog\FormWidgets\BlogMarkdown
+            cssClass: 'field-slim blog-post-preview'
+            stretch: true
+            mode: split
+        categories:
+            nameFrom: name
+            descriptionFrom: description
+            span: left
+            type: relation
+            commentAbove: 'rainlab.blog::lang.post.categories_comment'
+            tab: 'rainlab.blog::lang.post.tab_categories'
+        published:
+            label: 'rainlab.blog::lang.post.published'
+            span: left
+            type: checkbox
+            tab: 'rainlab.blog::lang.post.tab_manage'
+        featured:
+            label: 'показать в  слайдере на главной странице'
+            span: auto
+            type: checkbox
+            comment: 'не отмечать  если не нужно выставлять на слайдере'
+            tab: 'rainlab.blog::lang.post.tab_manage'
+        user:
+            tab: 'rainlab.blog::lang.post.tab_manage'
+            label: 'rainlab.blog::lang.post.published_by'
+            span: right
+            type: dropdown
+            emptyOption: 'rainlab.blog::lang.post.current_user'
+        published_at:
+            tab: 'rainlab.blog::lang.post.tab_manage'
+            label: 'rainlab.blog::lang.post.published_on'
+            span: left
+            cssClass: checkbox-align
+            type: datepicker
+            mode: datetime
+            trigger:
+                action: enable
+                field: published
+                condition: checked
+        excerpt:
+            tab: 'rainlab.blog::lang.post.tab_manage'
+            label: 'rainlab.blog::lang.post.excerpt'
+            type: textarea
+            size: small
+        featured_images:
+            label: 'rainlab.blog::lang.post.featured_images'
+            mode: image
+            imageWidth: 200
+            imageHeight: 200
+            useCaption: true
+            thumbOptions:
+                mode: crop
+                extension: auto
+            span: left
+            type: fileupload
+            dependsOn: type
+            tab: 'rainlab.blog::lang.post.tab_manage'
+        type:
+            label: Type
+            options:
+                photo: Photo
+                video: Video
+            span: auto
+            default: photo
+            type: balloon-selector
+            tab: 'rainlab.blog::lang.post.tab_manage'
+        video:
+            label: Video
+            mode: file
+            span: left
+            type: mediafinder
+            dependsOn: type
+            tab: 'rainlab.blog::lang.post.tab_manage'
diff --git a/plugins/rainlab/blog/updates/builder_table_update_rainlab_blog_posts_2.php b/plugins/rainlab/blog/updates/builder_table_update_rainlab_blog_posts_2.php
new file mode 100644
index 0000000..14dbfe1
--- /dev/null
+++ b/plugins/rainlab/blog/updates/builder_table_update_rainlab_blog_posts_2.php
@@ -0,0 +1,25 @@
+string('type')->nullable();
+            $table->string('video')->nullable();
+        });
+    }
+    
+    public function down()
+    {
+        Schema::table('rainlab_blog_posts', function($table)
+        {
+            $table->dropColumn('type');
+            $table->dropColumn('video');
+        });
+    }
+}
diff --git a/plugins/rainlab/blog/updates/builder_table_update_rainlab_blog_posts_3.php b/plugins/rainlab/blog/updates/builder_table_update_rainlab_blog_posts_3.php
new file mode 100644
index 0000000..2a45234
--- /dev/null
+++ b/plugins/rainlab/blog/updates/builder_table_update_rainlab_blog_posts_3.php
@@ -0,0 +1,23 @@
+string('type', 191)->default('photo')->change();
+        });
+    }
+    
+    public function down()
+    {
+        Schema::table('rainlab_blog_posts', function($table)
+        {
+            $table->string('type', 191)->default(null)->change();
+        });
+    }
+}
diff --git a/plugins/rainlab/blog/updates/version.yaml b/plugins/rainlab/blog/updates/version.yaml
index 70bbb90..2e58c67 100644
--- a/plugins/rainlab/blog/updates/version.yaml
+++ b/plugins/rainlab/blog/updates/version.yaml
@@ -70,3 +70,9 @@
 1.6.3:
     - 'Updated table rainlab_blog_posts'
     - builder_table_update_rainlab_blog_posts.php
+1.6.4:
+    - 'Updated table rainlab_blog_posts'
+    - builder_table_update_rainlab_blog_posts_2.php
+1.6.5:
+    - 'Updated table rainlab_blog_posts'
+    - builder_table_update_rainlab_blog_posts_3.php
diff --git a/themes/hhm/pages/home.htm b/themes/hhm/pages/home.htm
new file mode 100644
index 0000000..c85b2a9
--- /dev/null
+++ b/themes/hhm/pages/home.htm
@@ -0,0 +1,6 @@
+title = "home"
+url = "/"
+is_hidden = 0
+robot_index = "index"
+robot_follow = "follow"
+==
\ No newline at end of file
diff --git a/themes/hhm/theme.yaml b/themes/hhm/theme.yaml
new file mode 100644
index 0000000..0865688
--- /dev/null
+++ b/themes/hhm/theme.yaml
@@ -0,0 +1,5 @@
+name: hhm
+description: ''
+author: ''
+homepage: ''
+code: ''