Gogs преди 3 седмици
родител
ревизия
a8f8181990
променени са 5 файла, в които са добавени 486 реда и са изтрити 31 реда
  1. 105 28
      README.md
  2. 202 0
      vue/app/pages/Page/index.coffee
  3. 96 0
      vue/app/pages/Page/index.pug
  4. 80 0
      vue/app/pages/Page/index.styl
  5. 3 3
      vue/app/temp.coffee

+ 105 - 28
README.md

@@ -1,9 +1,7 @@
 # Текущая задача
 
-опиши app/pages/pages как универсальный компонент для VueRouter, который будет получать из него урл, загружая запись блога в соответствии с привязанным URL, и её отображать. 
-Учти что обработчик работает только с отдельными страницами, для записей blog_post, event, product используются отдельные обработчики
-Учти что в записи блога могут быть описаны используемые компоненты. Учти мультиязычность проекта, и вынеси все необходимые надписи в объект настроек, загружаемый из couchdb, также добавь в него настройки URL и того какие записи блога показывать, доработай его описание в https://gogs.osvoj.ru/s5l.ru/borbad.s5l.ru/raw/master/README.md (слова вынеси в ассоциируемый массив, так как их состав может дополняться для слов необходимых для используемого компонента)
-основной контент пишется на markdown, нужно добавить в него тег определяющий отдельные классы страниц. 
+Доработай  app/pages/pages с учётом изменений в сструктуре хранимых данных, для объекта страниц. в  https://gogs.osvoj.ru/s5l.ru/borbad.s5l.ru/raw/master/README.md
+продумай создание структуры траницы с подключаемыми компанентами, в теле markdown текста.
 
 # файл с правилами
 https://gogs.osvoj.ru/s5l.ru/borbad.s5l.ru/raw/master/README.md
@@ -225,11 +223,15 @@ app/
 |     ├── CouchdbClass.coffee
 |     ...
 ├── page/
-|     ├── Home/  (главная страница)
+|     ├── pages/  (универсальный компонент для отображения страниц сайта)
 |     |     ├── index.coffee
 |     |     ├── index.pug
 |     |     ├── index.styl
-|     ├── [другие_страницы]/
+|     ├── blog/  (компонент для отображения главной страницы блога)
+|     |     ├── index.coffee
+|     |     ├── index.pug
+|     |     ├── index.styl
+|     ├── [другие_компоненты]/   (компоненты для отображения страниц,  специаьного типа (События, Продукты...))
 |           ├── index.coffee
 |           ├── index.pug
 |           ├── index.styl
@@ -268,9 +270,6 @@ app/
             ├── index.pug
             ├── index.styl
 
-
-Проанализирую компоненты и опишу все необходимые глобальные переменные, которые должны быть доступны через `_`.
-
 ## Глобальные переменные состояния приложения
 
 ### Основные глобальные переменные в `_`
@@ -751,6 +750,7 @@ module.exports =
 ## Описание всех хранимых объектов
 
 Базовый объект "Запись блога" (blog_post)
+```
 coffee
 {
     _id: "blog_post_season_opening_2024_borbad"
@@ -787,7 +787,69 @@ coffee
     likes: 23
     shares: 45
 }
+```
+Наследник "Страница" (page) - расширяет blog_post
+```
+coffee
+{
+    _id: "page_about_borbad"
+    type: "page"
+    domain: ["borbad.s5l.ru", "concert-hall.tj"]
+    language: ["ru", "en", "tj"]
+    title: ["О нас - Кохи Борбад", "About Us - Borbad Concert Hall", "Дар бораи мо - Ҳолли Борбад"]
+    slug: ["about", "about", "dar-borai-mo"]
+    content: [
+        "# О концертном зале Борбад\n\n[class:bg-blue-50 p-6 rounded-lg shadow-md]Мы рады приветствовать вас в нашем концертном зале.[/class] \n\n app-link(:to="/") Главная",
+        "# About Borbad Concert Hall\n\n[class:bg-blue-50 p-6 rounded-lg shadow-md]We are pleased to welcome you to our concert hall.[/class] \n\n app-link(:to="/") Main",
+        "# Дар бораи ҳолли консертии Борбад\n\n[class:bg-blue-50 p-6 rounded-lg shadow-md]Мо шодем, ки шуморо дар ҳолли консертии мо пазируфтаем.[/class] \n\n app-link(:to="/") Главная"
+    ]
+    excerpt: ["Информация о концертном зале Борбад в Душанбе", "Information about Borbad Concert Hall in Dushanbe", "Маълумот дар бораи ҳолли консертии Борбад дар Душанбе"]
+    image: [
+        "/assets/borbad.s5l.ru/pages/about.jpg"
+        "/assets/borbad.s5l.ru/pages/about.jpg"
+        "/assets/borbad.s5l.ru/pages/about.jpg"
+    ]
+    gallery: [
+        [
+            "/assets/borbad.s5l.ru/gallery/about1.jpg"
+            "/assets/borbad.s5l.ru/gallery/about2.jpg"
+        ],
+        [
+            "/assets/borbad.s5l.ru/gallery/about1.jpg"
+            "/assets/borbad.s5l.ru/gallery/about2.jpg"
+        ],
+        [
+            "/assets/borbad.s5l.ru/gallery/about1.jpg"
+            "/assets/borbad.s5l.ru/gallery/about2.jpg"
+        ]
+    ]
+    seo: {
+        description: ["Узнайте больше о концертном зале Борбад в Душанбе", "Learn more about Borbad Concert Hall in Dushanbe", "Маълумоти бештар дар бораи ҳолли консертии Борбад дар Душанбе"]
+        keywords: [
+            ["концертный зал", "Борбад", "Душанбе", "культура"],
+            ["concert hall", "Borbad", "Dushanbe", "culture"],
+            ["ҳолли консертӣ", "Борбад", "Душанбе", "фарҳанг"]
+        ]
+        title: ["О нас - Концертный зал Борбад", "About Us - Borbad Concert Hall", "Дар бораи мо - Ҳолли Борбад"]
+    }
+    components: {
+            app-link: 'AppLink'
+            formvalidator: 'FormValidator'
+    }
+    parent_id: null
+    parent_path: []
+    order: 1
+    status: "published"
+    protected: false
+    show_in_sitemap: true
+    allow_comments: false
+    featured: false
+    created_at: "2024-01-15T10:00:00.000Z"
+    updated_at: "2024-01-15T10:00:00.000Z"
+}
+```
 Наследник "Событие" (event) - расширяет blog_post
+```
 coffee
 {
     _id: "event_beethoven_concert_2024_03_borbad"
@@ -852,7 +914,9 @@ coffee
     published_at: "2024-01-15T10:00:00.000Z"
     views: 289
 }
+```
 Наследник "Товар" (product) - расширяет blog_post
+```
 coffee
 {
     _id: "product_tshirt_logo_2024_borbad"
@@ -920,7 +984,9 @@ coffee
     published_at: "2024-01-15T10:00:00.000Z"
     views: 134
 }
+```
 Наследник "Слайдер" (slide) - расширяет blog_post
+```
 coffee
 {
     _id: "slide_01_borbad"
@@ -964,7 +1030,9 @@ coffee
     published_at: "2024-01-01T00:00:00.000Z"
     views: 0                                             # Для слайдеров обычно не отслеживается
 }
+```
 Категория (category) - иерархическая структура
+```
 coffee
 {
     _id: "category_classical_music_concerts_events_borbad"
@@ -991,7 +1059,9 @@ coffee
     created_at: "2024-01-15T10:00:00.000Z"
     updated_at: "2024-01-15T10:00:00.000Z"
 }
+```
 Настройки домена (domain_settings)
+```
 coffee
 {
     _id: "domain_settings_borbad_s5l_ru"
@@ -1028,10 +1098,30 @@ coffee
             ecommerce: true
         }
     }
+    pages: {
+        urls: {
+            about: '/about'
+            contacts: '/contacts'
+            privacy: '/privacy' 
+            terms: '/terms'
+            help: '/help'
+        }
+        strings: {
+            not_found: ['Страница не найдена', 'Page not found', 'Саҳифа ёфт нашуд']
+            back_to_home: ['Вернуться на главную', 'Back to home', 'Бозгашт ба саҳифаи асосӣ']
+            loading: ['Загрузка...', 'Loading...', 'Бор шуда истодааст...']
+            page_not_found_description: ['Запрашиваемая страница не существует или была перемещена', 'The requested page does not exist or has been moved', 'Саҳифаи дархостшуда вуҷуд надорад ё кӯчонида шудааст']
+            gallery: ['Галерея', 'Gallery', 'Галерея']
+            read_more: ['Читать далее', 'Read more', 'Бештар хонед']
+            share: ['Поделиться', 'Share', 'Мубодила']
+        }
+    }
     created_at: "2024-01-15T10:00:00.000Z"
     updated_at: "2024-01-15T10:00:00.000Z"
 }
+```
 Пользователь (user)
+```
 coffee
 {
     _id: "user_admin_main"
@@ -1064,7 +1154,9 @@ coffee
     updated_at: "2024-01-15T10:00:00.000Z"
     last_login: "2024-01-15T09:30:00.000Z"
 }
+```
 Заказ (order)
+```
 coffee
 {
     _id: "order_2024_001_borbad"
@@ -1119,25 +1211,10 @@ coffee
     created_at: "2024-01-15T13:45:00.000Z"
     updated_at: "2024-01-15T14:00:00.000Z"
 }
-Настройка (setting)
-coffee
-{
-    _id: "setting_seo_title_borbad_s5l_ru"
-    type: "setting"
-    domain: "borbad.s5l.ru"
-    language: ["ru", "en"]
-    key: "seo_title"
-    value: ["Кохи Борбад - Концертный зал Душанбе", "Borbad Concert Hall - Dushanbe"]
-    value_type: "string"                                  # string | number | boolean | object | array
-    is_global: false
-    description: ["Заголовок сайта для SEO", "Site title for SEO"]
-    category: "seo"
-    group: "site_settings"
-    editable: true
-    created_at: "2024-01-01T00:00:00.000Z"
-    updated_at: "2024-01-15T10:00:00.000Z"
-}
+```
+
 Аудит (audit_log)
+```
 coffee
 {
     _id: "audit_2024_001"
@@ -1161,6 +1238,6 @@ coffee
     }
     created_at: "2024-01-15T10:00:00.000Z"
 }
-
+```
 ## _desing документ для работы с данными
 https://gogs.osvoj.ru/s5l.ru/borbad.s5l.ru/raw/master/scripts/design-documents.coffee

+ 202 - 0
vue/app/pages/Page/index.coffee

@@ -0,0 +1,202 @@
+# Загрузка стилей компонента
+document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss"  page="Page">'+stylFns['app/pages/Page/index.styl']+'</style>')
+
+# Подключение markdown парсера
+marked = require 'marked'
+
+module.exports =
+    name: 'Page'
+    render: (new Function '_ctx', '_cache', renderFns['app/pages/Page/index.pug'])()
+    
+    data: ->
+        page: null
+        loading: true
+        error: null
+        notFound: false
+        pageSettings: {}
+    
+    computed:
+        # Текущий язык из глобального состояния
+        currentLanguage: ->
+            return _.currentLanguage || 'ru'
+        
+        # Настройки сайта из глобального состояния
+        siteSettings: ->
+            return _.appState?.siteSettings || {}
+        
+        # Обработанный markdown контент
+        processedContent: ->
+            if not @page?.content
+                return ''
+            
+            # Получаем контент для текущего языка
+            content = @getMultilingualText(@page.content, '')
+            return @processMarkdown(content)
+        
+        # Мета-данные для SEO
+        pageMeta: ->
+            if not @page
+                return {}
+            
+            return {
+                title: @getMultilingualText(@page.seo?.title, @getMultilingualText(@page.title, '')),
+                description: @getMultilingualText(@page.seo?.description, @getMultilingualText(@page.excerpt, '')),
+                keywords: @getMultilingualText(@page.seo?.keywords, []).join(', ')
+            }
+    
+    beforeMount: ->
+        @loadPageSettings()
+        @loadPageData()
+    
+    watch:
+        '$route.params.slug': ->
+            @loadPageData()
+        
+        'currentLanguage': ->
+            @loadPageData()
+    
+    methods:
+        # Загрузка настроек страниц
+        loadPageSettings: ->
+            @pageSettings = _.appState?.siteSettings?.pages || {
+                urls: {
+                    about: '/about'
+                    contacts: '/contacts' 
+                    privacy: '/privacy'
+                    terms: '/terms'
+                }
+                components: {
+                    about: 'AboutPage'
+                    contacts: 'ContactsPage'
+                }
+                strings: {
+                    not_found: ['Страница не найдена', 'Page not found', 'Саҳифа ёфт нашуд']
+                    back_to_home: ['Вернуться на главную', 'Back to home', 'Бозгашт ба саҳифаи асосӣ']
+                    loading: ['Загрузка...', 'Loading...', 'Бор шуда истодааст...']
+                }
+            }
+        
+        # Загрузка данных страницы
+        loadPageData: ->
+            @loading = true
+            @error = null
+            @notFound = false
+            
+            slug = @$route.params.slug
+            if not slug
+                @error = "Не указан slug страницы"
+                @loading = false
+                return
+            
+            @loadPageBySlug(slug).then (page) =>
+                if page
+                    @.page = page
+                    @updatePageMeta()
+                else
+                    @.notFound = true
+                @loading = false
+            .catch (error) =>
+                @error = "Ошибка загрузки страницы: "+error
+                @loading = false
+        
+        # Поиск страницы по slug
+        loadPageBySlug: (slug) ->
+            return new Promise (resolve, reject) =>
+                try
+                    # Ищем в кэше глобального состояния
+                    cachedPages = _.appState?.pages || []
+                    cachedPage = cachedPages.find (page) =>
+                        slugs = @getMultilingualText(page.slug, [])
+                        return slugs.includes(slug)
+                    
+                    if cachedPage
+                        resolve(cachedPage)
+                        return
+                    
+                    # Если нет в кэше, загружаем из базы
+                    AppDB.db.query('pages/by_slug_multilingual', {
+                        key: ['borbad.s5l.ru', @currentLanguage, slug]
+                        include_docs: true
+                    }).then (result) =>
+                        if result.rows.length > 0
+                            page = AppDB.processMultilingualDocument(result.rows[0].doc)
+                            # Сохраняем в кэш
+                            if not _.appState.pages
+                                _.appState.pages = []
+                            _.appState.pages.push(page)
+                            resolve(page)
+                        else
+                            resolve(null)
+                    .catch (error) ->
+                        reject(error)
+                        
+                catch error
+                    reject(error)
+        
+        # Обработка markdown с поддержкой кастомных классов
+        processMarkdown: (content) ->
+            if not content
+                return ''
+            
+            # Обрабатываем кастомные теги для классов
+            processedContent = content.replace(/\[class:([^\]]+)\]/g, '<div class="$1">')
+                                     .replace(/\[\/class\]/g, '</div>')
+            
+            # Настройка marked
+            marked.setOptions({
+                breaks: true
+                gfm: true
+                sanitize: false  # Разрешаем HTML для кастомных классов
+            })
+            
+            return marked(processedContent)
+        
+        # Получение мультиязычного текста
+        getMultilingualText: (textArray, fallback = '') ->
+            return AppDB.multilingual.getText(textArray, fallback)
+        
+        # Получение локализованной строки из настроек
+        getLocalizedString: (key, fallback = '') ->
+            strings = @pageSettings.strings?[key] || []
+            return @getMultilingualText(strings, fallback)
+        
+        # Обновление мета-данных страницы
+        updatePageMeta: ->
+            if @pageMeta.title
+                document.title = @pageMeta.title + ' - Кохи Борбад'
+            
+            # Обновляем meta теги
+            metaDescription = document.querySelector('meta[name="description"]')
+            if not metaDescription
+                metaDescription = document.createElement('meta')
+                metaDescription.name = 'description'
+                document.head.appendChild(metaDescription)
+            metaDescription.content = @pageMeta.description
+            
+            metaKeywords = document.querySelector('meta[name="keywords"]')
+            if not metaKeywords
+                metaKeywords = document.createElement('meta')
+                metaKeywords.name = 'keywords'
+                document.head.appendChild(metaKeywords)
+            metaKeywords.content = @pageMeta.keywords
+        
+        # Рендер компонента указанного в настройках страницы
+        renderCustomComponent: ->
+            if not @page?.settings?.component
+                return null
+            
+            componentName = @page.settings.component
+            try
+                component = require('app/pages/' + componentName)
+                return component
+            catch error
+                debug.log "Компонент "+componentName+" не найден: "+error
+                return null
+        
+        # Обработка клика по внутренним ссылкам в контенте
+        handleContentClick: (event) ->
+            if event.target.tagName == 'A'
+                href = event.target.getAttribute('href')
+                if href and href.startsWith('/')
+                    event.preventDefault()
+                    _.$router.push(href)

+ 96 - 0
vue/app/pages/Page/index.pug

@@ -0,0 +1,96 @@
+div(class="page-container")
+    //- Состояние загрузки
+    div(v-if="loading" class="container mx-auto px-4 py-16 text-center")
+        div(class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto")
+        p(class="mt-4 text-gray-600") {{ getLocalizedString('loading') }}
+    
+    //- Состояние ошибки
+    div(v-else-if="error" class="container mx-auto px-4 py-16")
+        div(class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded max-w-2xl mx-auto")
+            p {{ error }}
+            app-link(
+                to="/" 
+                class="mt-4 inline-block bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition-colors"
+            ) {{ getLocalizedString('back_to_home') }}
+    
+    //- Страница не найдена
+    div(v-else-if="notFound" class="container mx-auto px-4 py-16 text-center")
+        div(class="max-w-2xl mx-auto")
+            h1(class="text-6xl font-bold text-gray-300 mb-4") 404
+            h2(class="text-2xl font-semibold text-gray-800 mb-4") {{ getLocalizedString('not_found') }}
+            p(class="text-gray-600 mb-8") {{ getLocalizedString('page_not_found_description', 'Запрашиваемая страница не существует или была перемещена') }}
+            app-link(
+                to="/" 
+                class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors inline-block"
+            ) {{ getLocalizedString('back_to_home') }}
+    
+    //- Отображение страницы
+    div(v-else-if="page" class="page-content")
+        //- Кастомный компонент если указан
+        component(
+            v-if="renderCustomComponent()"
+            :is="renderCustomComponent()"
+            :page="page"
+            :settings="pageSettings"
+        )
+        
+        //- Стандартное отображение
+        div(v-else)
+            //- Hero секция если есть изображение
+            section(v-if="page.image" class="page-hero relative bg-gray-900")
+                img(
+                    :src="getMultilingualText(page.image)" 
+                    :alt="getMultilingualText(page.title)"
+                    class="w-full h-64 md:h-96 object-cover opacity-70"
+                )
+                div(class="absolute inset-0 flex items-center justify-center")
+                    div(class="text-center text-white")
+                        h1(class="text-4xl md:text-6xl font-bold mb-4") {{ getMultilingualText(page.title) }}
+                        p(v-if="getMultilingualText(page.excerpt)" class="text-xl opacity-90 max-w-2xl mx-auto") {{ getMultilingualText(page.excerpt) }}
+            
+            //- Контент страницы
+            div(:class="{ 'container mx-auto px-4 py-16': !page.image }")
+                //- Заголовок если нет hero изображения
+                div(v-if="!page.image" class="text-center mb-12")
+                    h1(class="text-4xl font-bold text-gray-800 mb-4") {{ getMultilingualText(page.title) }}
+                    p(v-if="getMultilingualText(page.excerpt)" class="text-xl text-gray-600 max-w-2xl mx-auto") {{ getMultilingualText(page.excerpt) }}
+                
+                //- Основной контент
+                article(
+                    class="prose prose-lg max-w-none prose-headings:text-gray-800 prose-p:text-gray-600 prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline prose-ul:text-gray-600 prose-ol:text-gray-600 prose-strong:text-gray-800 prose-blockquote:border-blue-600 prose-blockquote:text-gray-600"
+                    v-html="processedContent"
+                    @click="handleContentClick"
+                )
+                
+                //- Галерея если есть
+                div(v-if="page.gallery && page.gallery[0] && page.gallery[0].length > 0" class="mt-12")
+                    h2(class="text-2xl font-semibold text-gray-800 mb-6") {{ getLocalizedString('gallery', 'Галерея') }}
+                    div(class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4")
+                        img(
+                            v-for="(image, index) in getMultilingualText(page.gallery, [])" 
+                            :key="index"
+                            :src="image"
+                            :alt="getMultilingualText(page.title) + ' - изображение ' + (index + 1)"
+                            class="w-full h-48 object-cover rounded-lg shadow-md hover:shadow-lg transition-shadow cursor-pointer"
+                            @click="openImageGallery(index)"
+                        )
+    
+    //- Модальное окно для галереи
+    div(v-if="showGalleryModal" class="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50 p-4")
+        div(class="relative max-w-4xl max-h-full")
+            button(
+                @click="showGalleryModal = false"
+                class="absolute -top-12 right-0 text-white text-2xl hover:text-gray-300 transition-colors"
+            ) ×
+            img(
+                :src="currentGalleryImage"
+                class="max-w-full max-h-full object-contain"
+            )
+            div(v-if="getMultilingualText(page.gallery, []).length > 1" class="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex space-x-2")
+                button(
+                    v-for="(image, index) in getMultilingualText(page.gallery, [])"
+                    :key="index"
+                    @click="currentGalleryIndex = index"
+                    :class="{ 'bg-blue-600': currentGalleryIndex === index, 'bg-white': currentGalleryIndex !== index }"
+                    class="w-3 h-3 rounded-full transition-colors"
+                )

+ 80 - 0
vue/app/pages/Page/index.styl

@@ -0,0 +1,80 @@
+// Стили для компонента Page
+
+.page-container
+  min-height: 60vh
+
+.page-hero
+  position: relative
+  
+  &::after
+    content: ''
+    position: absolute
+    bottom: 0
+    left: 0
+    right: 0
+    height: 100px
+    background: linear-gradient(to bottom, transparent, rgba(0,0,0,0.5))
+    pointer-events: none
+
+// Стили для markdown контента
+.page-content
+  // Кастомные классы через markdown теги
+  .content-section
+    margin: 2rem 0
+    padding: 1.5rem
+    border-radius: 8px
+    
+    &.bg-gray-50
+      background-color: #f9fafb
+    
+    &.bg-blue-50  
+      background-color: #eff6ff
+    
+    &.shadow-md
+      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1)
+  
+  // Стили для кастомных компонентов в контенте
+  .custom-component
+    margin: 2rem 0
+    border: 1px solid #e5e7eb
+    border-radius: 8px
+    overflow: hidden
+    
+    &.interactive
+      transition: all 0.3s ease
+      
+      &:hover
+        transform: translateY(-2px)
+        box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1)
+
+// Адаптивные стили
+@media (max-width: 768px)
+  .page-hero
+    h1
+      font-size: 2.5rem !important
+    
+    p
+      font-size: 1.1rem !important
+
+// Анимации
+.fade-enter-active,
+.fade-leave-active
+  transition: opacity 0.3s ease
+
+.fade-enter-from,
+.fade-leave-to
+  opacity: 0
+
+// Темная тема
+@media (prefers-color-scheme: dark)
+  .dark
+    .page-content
+      article
+        @apply prose-invert
+      
+      .content-section
+        &.bg-gray-50
+          background-color: #374151
+        
+        &.bg-blue-50
+          background-color: #1e3a8a

+ 3 - 3
vue/app/temp.coffee

@@ -268,16 +268,16 @@ globalThis.AppDB = new AppDatabase()
 globalThis.Multilingual = new MultilingualData()
 
 # Маршруты
+
 routes = [
-  { path: '/', component: require 'app/pages/Home' }
+  { path: '/', component: require 'app/pages/Page' }
   { path: '/events', component: require 'app/pages/Events' }
   { path: '/events/:id', component: require 'app/pages/EventDetail' }
 #  { path: '/blog', component: require 'app/pages/Blog' }
 #  { path: '/blog/:id', component: require 'app/pages/BlogDetail' }
 #  { path: '/products', component: require 'app/pages/Products' }
 #  { path: '/products/:id', component: require 'app/pages/ProductDetail' }
-  { path: '/about', component: require 'app/pages/About' }
-  { path: '/contacts', component: require 'app/pages/Contacts' }
+  { path: '/:slug', component: require 'app/pages/Page' }  # Универсальный обработчик страниц
 ]
 
 # Глобальное определение vuejs приложения