От автора: это руководство предназначено для начинающих и профессионалов, которые хотят создать полноценный многоязычный веб-сайт с помощью Nuxt.js. С использованием этого пошагового руководства вы получите динамический веб-сайт Nuxt.js, работающий на сервере now, с использованием API для многоязычного контента.
Если вы спешите, то можете скачать весь проект (nuxtblok.now.sh) на Github.
Настройка среды
Что вам потребуется:
Базовое понимание NuxtJs
VueJs и их CLI
NodeJS
NPM
NPX
CLI now.sh для хостинга
Аккаунт на Storyblok.com для управления контентом
Если это еще не сделано, установите NodeJs, NPM и NPX. Начнем с инициализации проекта с помощью начального шаблона nuxt.js.
1 2 3 |
npx create-nuxt-app mywebsite cd mywebsite && npm build npm run dev |
Nuxt.js по умолчанию запускает свой сервер на порту 3000, поэтому после запуска npm запустите dev и откройте браузер по адресу //localhost:3000.
Поскольку мы будем использовать SCSS для организации CSS, нам также нужно установить sass-загрузчик.
1 |
npm install --save-dev sass-loader node-sass css-loader |
Чтобы отслеживать изменения, которые мы вносим с течением времени, мы также инициализируем репозиторий git.
1 2 |
// Initialize git git init && git add . && git commit -m 'init' |
Построение каркаса
Мы начнем строить каркас для сайта. В конце у вас будет заголовок, разделы хэдера и футера и несколько полезных глобальных утилит CSS-классов.
Глобальный SCSS в Nuxt.js
На шаге 1 мы установили загрузчик SCSS, поэтому давайте создадим некоторые глобальные стили и определим переменные scss. Мы создадим папку для стилизации общих HTML-тегов assets/scss/elements/ и другую папку для служебного компонента assets/scss/components/.
1 2 3 4 5 6 7 8 9 |
assets/ --| scss/ -----| elements/ --------| body.scss --------| ... -----| components/ --------| util.scss --------| ... --| styles.scss |
Создайте файл assets/scss/styles.scss и добавьте следующий в него код.
1 2 3 4 5 6 7 8 9 10 11 12 |
$brand-color: #357F8A; $breakpoint-small: 480px; $breakpoint-medium: 768px; $breakpoint-large: 960px; $breakpoint-xlarge: 1220px; $breakpoint-mini-max: ($breakpoint-small - 1); $breakpoint-small-max: ($breakpoint-medium - 1); $breakpoint-medium-max: ($breakpoint-large - 1); $breakpoint-large-max: ($breakpoint-xlarge - 1); @import 'elements/body.scss'; @import 'components/util.scss'; |
Вместо того, чтобы помещать стили всех HTML элементов в один файл, я предпочитаю создавать отдельные файлы, чтобы сделать проект структурированным и масштабируемым. Создайте файл assets/scss/elements/body.scss для определения базовых стилей шрифтов.
1 2 3 4 5 6 7 8 |
body { font-family: 'Zilla Slab', Helvetica, sans-serif; line-height: 1; font-size: 18px; color: #000; margin: 0; padding: 0; } |
В папке components мы управляем глобальными компонентами css и вспомогательными классами. Создайте файл assets/scss/components/util.scss для определения глобальных служебных классов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
.util__flex { display: flex; } .util__flex-col { flex: 0 0 auto; } .util__flex-eq { flex: 1; } .util__container { max-width: 75rem; margin-left: auto; margin-right: auto; padding-left: 20px; padding-right: 20px; box-sizing: border-box; } |
Добавление Google font в Nuxt.js
В файле body.scss мы определили Zilla Slab как шрифт. Поскольку это не системный шрифт, мы должны добавить его в раздел заголовка документа. Здесь в игру вступает файл конфигурации Nuxt.js. Откройте nuxt.config.js и добавьте таблицу стилей шрифта в раздел head.
1 2 3 4 5 6 7 8 9 10 11 |
head: { ... link: [ ... { rel: 'stylesheet', href: '//fonts.googleapis.com/css?family=Zilla+Slab:400,700' } ] }, ... |
Определение макета по умолчанию
Теперь, когда у нас есть SCSS, нам нужно добавить его в проект. Убедитесь, что вы установили загрузчик sass на первом этапе и замените код в layouts/default.vue на следующий.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<template> <div> <top-header/> <main id="main" role="main"> <nuxt/> </main> <bottom-footer/> </div> </template> <script> import TopHeader from '~/components/TopHeader.vue' import BottomFooter from '~/components/BottomFooter.vue' export default { components: { TopHeader, BottomFooter } } </script> <style lang="scss"> @import '../assets/scss/styles.scss'; </style> |
Вы увидите сообщение об ошибке, гласящее, что компоненты TopHeader.vue и BottomFooter.vue еще не существуют. Итак, давайте создадим их.
Создание компонента заголовка
Обратите внимание на атрибут lang=»scss» в теге style. Он позволяет использовать SCSS в компонентах Vue.js.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
<template> <header class="top-header util__flex util__container"> <nav class="top-header__col"> <ul class="nav"> <li> <nuxt-link class="nav__item" to="/">Home</nuxt-link> </li> <li> <nuxt-link class="nav__item" to="/en/blog">Blog</nuxt-link> </li> </ul> </nav> <a href="/" class="top-header__col top-header__logo"> <img src="//a.storyblok.com/f/42016/1096x313/0353bf6654/logo2.png"> </a> <nav class="top-header__col top-header__second-navi"> <ul class="nav"> <li> <nuxt-link class="nav__item" to="/en/blog">English</nuxt-link> </li> <li> <nuxt-link class="nav__item" to="/de/blog">German</nuxt-link> </li> </ul> </nav> </header> </template> <style lang="scss"> .top-header { justify-content: space-between; padding-top: 30px; padding-bottom: 30px; } .top-header__logo { text-align: center; position: absolute; left: 50%; img { position: relative; max-height: 60px; left: -50%; top: -15px; } } .top-header__second-navi { text-align: right; } </style> |
Создание компонента футера
Добавьте в папку ./components файл BottomFooter.vue.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<template> <footer class="bottom-footer"> <div class="util__container"> <nuxt-link class="bottom-footer__link" to="/en/sitemap">Sitemap</nuxt-link> </div> </footer> </template> <style lang="scss"> .bottom-footer { background: #e3f2ed; padding: 40px 0 120px 0; text-align: center; } .bottom-footer__link { color: #8ba19a; text-decoration: none; } </style> |
В настоящий момент сайт должен выглядеть примерно так, как показано на следующем скриншоте. На следующем шаге я покажу вам, как создать домашнюю страницу с тизером и разделом функций.
Теперь давайте передадим это в Git. Смотрите для справки мой коммит GitHub.
1 |
$ git add . && git commit -m 'creates the skeleton' |
Создание главной страницы
Установите модуль Nuxt.js Storyblok. Модуль Storyblok установит $storyapi и $storyblok в экземпляр Vue.
1 |
$ npm install storyblok-nuxt --save |
После установки модуля вам необходимо инициализировать его токеном предварительного просмотра вашего пространства Storyblok. Зарегистрируйтесь или войдите на app.storyblok.com и создайте новое пространство. Добавьте следующее в файл nuxt.config.js и замените PREVIEW_TOKEN на токен предварительного просмотра.
1 2 3 4 5 |
module.exports = { modules: [ ['storyblok-nuxt', {accessToken: 'YOUR_PREVIEW_TOKEN', cacheProvider: 'memory'}] ], ... |
Обновите компонент домашней страницы
Теперь замените содержимое по умолчанию pages/index.vue следующим:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
<template> <section class="util__container"> <component v-if="story.content.component" :key="story.content._uid" :blok="story.content" :is="story.content.component"></component> </section> </template> <script> export default { data () { return { story: { content: {} } } }, mounted () { // Load the JSON from the API this.$storybridge.on(['input', 'published', 'change'], (event) => { if (event.action == 'input') { if (event.story.id === this.story.id) { this.story.content = event.story.content } } else { window.location.reload() } }) }, asyncData (context) { return context.app.$storyapi.get('cdn/stories/home', { version: 'draft' }).then((res) => { return res.data }).catch((res) => { if (!res.response) { console.error(res) context.error({ statusCode: 404, message: 'Failed to receive content form api' }) } else { console.error(res.response.data) context.error({ statusCode: res.response.status, message: res.response.data }) } }) } } </script> |
Метод asyncData загрузит JSON, который определяет, какие компоненты мы будем отображать на домашней странице.
Создание компонентов домашней страницы
Чтобы отобразить полную домашнюю страницу, нам нужно создать несколько компонентов. Добавьте файл components.js в папку plugins.
1 2 3 4 5 6 7 8 9 10 |
import Vue from 'vue' import Page from '~/components/Page.vue' import Teaser from '~/components/Teaser.vue' import Grid from '~/components/Grid.vue' import Feature from '~/components/Feature.vue' Vue.component('page', Page) Vue.component('teaser', Teaser) Vue.component('grid', Grid) Vue.component('feature', Feature) |
Nuxt.js не выбирает файлы в плагинах автоматически, поэтому нам нужно добавить их components.js в nuxt.config.js.
1 2 3 4 5 |
module.exports = { plugins: [ '~/plugins/components' ], ... |
Затем создайте компоненты Vue внутри папки components.
Page.vue
1 2 3 4 5 6 7 8 9 10 11 |
<template> <div v-editable="blok" class="page"> <component :key="blok._uid" v-for="blok in blok.body" :blok="blok" :is="blok.component"></component> </div> </template> <script> export default { props: ['blok'] } </script> |
Teaser.vue
1 2 3 4 5 6 7 8 9 10 11 |
<template> <div v-editable="blok"> {{ blok.headline }} </div> </template> <script> export default { props: ['blok'] } </script> |
Grid.vue
1 2 3 4 5 6 7 8 9 10 11 |
<template> <div v-editable="blok" class="util__flex"> <component :key="blok._uid" v-for="blok in blok.columns" :blok="blok" :is="blok.component"></component> </div> </template> <script> export default { props: ['blok'] } </script> |
Feature.vue
1 2 3 4 5 6 7 8 9 10 11 |
<template> <div v-editable="blok" class="util__flex-eq"> <h1>{{ blok.name }}</h1> </div> </template> <script> export default { props: ['blok'] } </script> |
При перезагрузке //localhost:3000/ вы должны увидеть следующее.
Создайте свой первый блок в Storyblok
Мы только что загрузили демонстрационный контент Storyblok, и теперь мы расширим компонент тизера интерактивными слайдами. Для этого начните с подключения среды к композитору Storyblok и вставьте хост разработки localhost:3000.
ВАЖНО: после вставки хоста вам нужно изменить поле реального пути (см. следующий шаг), в противном случае вы получите страницу 404.
Изменение поля реального пути
Теперь вы должны увидеть свой сайт на предварительном просмотре. Но он покажет страницу 404, потому что Storyblok по умолчанию использует путь к домашней странице /home. Чтобы это изменить, вам нужно будет перейти на вкладку Конфигурация и указать поле Реальный путь — / .
Итак, давайте определим схему нового блока слайдов компонента /. После добавления схемы и содержимого в Storyblok нам нужно добавить в проект компонент Vue.js slide. Создайте файл components/Slide.vue со следующим содержимым.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<template> <div class="slide" v-editable="blok"> <img :src="blok.image"> </div> </template> <script> export default { props: ['blok'] } </script> <style lang="scss"> .slide img { width: 100%; } </style> |
Добавьте новый компонент в файл component.js.
1 2 3 4 5 6 |
import Vue from 'vue' ... import Slide from '~/components/Slide.vue' ... Vue.component('slide', Slide) |
Конечно, мы не хотим отображать все слайды одновременно. Итак, давайте расширим логику Teaser.vue , чтобы показать точечную навигацию. Вы можете использовать любой плагин слайдера Vue.js, чтобы создать более продвинутый слайдер, но мы сделаем это просто.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
<template> <div v-editable="blok" class="teaser"> <component v-if="slide" :blok="slide" :is="slide.component"></component> <div class="teaser__pag"> <button @click="handleDotClick(index)" :key="index" v-for="(blok, index) in blok.body" :class="{'teaser__pag-dot--current': index == currentSlide}" class="teaser__pag-dot">Next</button> </div> </div> </template> <script> export default { props: ['blok'], data () { return { currentSlide: 0 } }, computed: { slide () { let slides = this.blok.body.filter((slide, index) => { return this.currentSlide === index }) if (slides.length) { return slides[0] } return null } }, methods: { handleDotClick (index) { this.currentSlide = index } } } </script> <style lang="scss"> .teaser__pag { width: 100%; text-align: center; margin: 30px 0; } .teaser__pag-dot { text-indent: -9999px; border: 0; border-radius: 50%; width: 17px; height: 17px; padding: 0; margin: 5px 6px; background-color: #ccc; -webkit-appearance: none; cursor: pointer; &--current { background-color: #000; } } </style> |
После сохранения у вас должен быть следующий результат.
Расширение раздела функций
Раздел функций в настоящее время содержит только заголовок. Теперь мы расширим его текстом описания и иконками. Нажмите на блок функций и добавьте поля description (с типом textarea) и icon (с типом изображения), нажав Определить схему.
Откройте компонент Feature (components/Feature.vue) и добавьте в него новые поля, а также некоторые базовые стили CSS.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<template> <div v-editable="blok" class="feature util__flex-eq"> <img :src="resizedIcon" class="feature__icon"> <h1>{{ blok.name }}</h1> <div class="feature__description"> {{ blok.description }} </div> </div> </template> <script> export default { computed: { resizedIcon () { if (typeof this.blok.icon !== 'undefined') { return '//img2.storyblok.com/80x80' + this.blok.icon.replace('//a.storyblok.com', '') } return null } }, props: ['blok'] } </script> <style lang="scss"> .feature { text-align: center; padding: 30px 10px 100px; } .feature__icon { max-width: 80px; } </style> |
После того, как вы заполнили некоторый контент, у вас должна быть полностью редактируемая домашняя страница.
Создание меню навигации
Для построения динамического меню навигации у вас есть несколько возможностей. Одним из них является создание глобального элемента контента, который содержит глобальные конфигурации. Другой способ — использовать API-интерфейс ссылок для автоматического создания навигации из дерева контента. В этом руководстве мы реализуем первый метод.
Поскольку мы создаем многоязычный веб-сайт, нам нужно создать глобальную конфигурацию для каждого языка. Начнем с создания папки на английском языке en.
Создание элемента контента глобальных настроек
Внутри папки en мы создаем элемент контента Settings с новым типом контента settings. Это будет элемент контента, в который мы помещаем элементы навигации и другие глобальные конфигурации сайта.
Измените реальный путь на / и создайте схему для основной навигации, определяющую ключ main_navi с типом Blocks.
Добавьте блок для элемента навигации с типом name — Text и типом link — Link. В конце элемент контента Settings должен выглядеть следующим образом:
Получение глобальных настроек в хранилище Vuex
Поскольку Nuxt.js поставляется со встроенной поддержкой Vuex, мы будем использовать его для получения и сохранения конфигурации навигации, а также текущего языка.
После отправки действия loadSettings в middleware у нас будут доступны элементы навигации по адресу $store.state.settings.main_navi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
export const state = () => ({ cacheVersion: '', language: 'en', settings: { main_navi: [] } }) export const mutations = { setSettings(state, settings) { state.settings = settings }, setLanguage(state, language) { state.language = language }, setCacheVersion(state, version) { state.cacheVersion = version } } export const actions = { loadSettings({ commit }, context) { return this.$storyapi.get(`cdn/stories/${context.language}/settings`, { version: context.version }).then((res) => { commit('setSettings', res.data.story.content) }) } } |
Добавление middleware
Middleware в Nuxt.js позволяет определить функцию, которая запускается перед отображением страницы. Функция может быть асинхронной и возвращать промис, поэтому она идеально подходит для загрузки настроек из API.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
export default function ({ app, isServer, route, store, isDev }) { let version = route.query._storyblok || isDev ? 'draft' : 'published' let language = route.params.language || 'en' if (isServer) { store.commit('setCacheVersion', app.$storyapi.cacheVersion) } if (!store.state.settings._uid || language !== store.state.language) { store.commit('setLanguage', language) return store.dispatch('loadSettings', {version: version, language: language}) } } |
Кроме того, middleware должно быть зарегистрировано в nuxt.config.js.
1 2 3 4 5 |
module.exports = { ... router: { middleware: 'languageDetection' }, |
Доступ к данным в компоненте TopHeader
Теперь в $store.state.settings.main_navi мы можем легко получить доступ к элементам навигации и циклически перемещать их, чтобы отобразить в components/TopHeader.vue.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<template> <header class="top-header util__flex util__container"> <nav class="top-header__col"> <ul class="top-header__nav"> <li :key="index" v-for="(navitem, index) in $store.state.settings.main_navi"> <nuxt-link class="top-header__link" :to="navitem.link.cached_url"> {{ navitem.name }} </nuxt-link> </li> </ul> </nav> <a href="/" class="top-header__col top-header__logo"> <img src="//a.storyblok.com/f/42016/1096x313/0353bf6654/logo2.png"> </a> <nav class="top-header__col top-header__second-navi"> <ul class="top-header__nav top-header__nav--right"> <li> <nuxt-link class="top-header__link" to="/en/blog">English</nuxt-link> </li> <li> <nuxt-link class="top-header__link" to="/de/blog">German</nuxt-link> </li> </ul> </nav> </header> </template> ... |
Перезагрузив страницу, мы должны увидеть заголовок навигации с настраиваемыми элементами навигации от Storyblok.
Создание раздела блога
Общей задачей при создании веб-сайта является разработка обзорной страницы коллекций, таких как новости, сообщения в блогах или товары. В нашем примере мы создадим простой блог. В Nuxt.js вы можете определить динамические маршруты, создавая папки с добавлением подчеркивания «_», и Nuxt автоматически преобразует их в маршруты Vue.js.
Окончательный URL должен выглядеть следующим образом /:language/blog/:slug, поэтому нам нужно создать следующую структуру папок.
1 2 3 4 5 6 |
pages/ --| _language/ -----| blog/ --------| _slug.vue --------| index.vue --| index.vue |
Добавление страницы с описанием блога
Мы начнем со страницы описания блога pages/_language/blog/_slug.vue, на которой будем извлекать контент из API, а затем выведем запись блога через markdown с использованием в качестве парсера marked. Итак, сначала нам нужно установить парсер markdown.
1 |
$ npm install marked --save |
Затем мы создадим файл pages/_language/blog/_slug.vue для динамического маршрута постов в блоге.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
<template> <section class="util__container"> <div v-editable="story.content" class="blog"> <h1>{{ story.content.name }}</h1> <div class="blog__body" v-html="body"> </div> </div> </section> </template> <script> import marked from 'marked' export default { data () { return { story: { content: { body: '' } } } }, computed: { body () { return marked(this.story.content.body) } }, mounted () { // Load the JSON from the API this.$storybridge.on(['input', 'published', 'change'], (event) => { if (event.action == 'input') { if (event.story.id === this.story.id) { this.story.content = event.story.content } } else { window.location.reload() } }) }, async asyncData (context) { return await context.app.$storyapi.get('cdn/stories/home', { version: 'draft' }).then((res) => { return res.response }).catch((res) => { if (!res.response) { console.error(res) context.error({ statusCode: 404, message: 'Failed to receive content form api' }) } else { console.error(res.response.data) context.error({ statusCode: res.response.status, message: res.response.data }) } }) } } </script> <style lang="scss"> .blog { padding: 0 20px; max-width: 600px; margin: 40px auto 100px; img { width: 100%; height: auto; } } .blog__body { line-height: 1.6; } </style> |
Создание страницы обзора
Чтобы составить список записей в блоге, мы создадим маршрут, /:language/blog, просто сохранив файл index.vue в папке blog.
API Storyblok может перечислять все элементы содержимого определенной папки с параметром starts_with. Количество возвращаемых элементов содержимого по умолчанию составляет 25, но вы можете изменить это с помощью параметра per_page и перейти к другим страницам с помощью параметра page.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
<template> <section class="util__container"> <div :key="blogPost.content._uid" v-for="blogPost in data.stories" class="blog__overview"> <h2> <nuxt-link class="blog__detail-link" :to="'/' + blogPost.full_slug"> {{ blogPost.content.name }} </nuxt-link> </h2> <small> {{ blogPost.published_at }} </small> <p> {{ blogPost.content.intro }} </p> </div> </section> </template> <script> export default { data () { return { total: 0, data: { stories: [] } } }, asyncData (context) { let version = context.query._storyblok || context.isDev ? 'draft' : 'published' return context.app.$storyapi.get('cdn/stories', { version: version, starts_with: `${context.store.state.language}/blog`, cv: context.store.state.cacheVersion }).then((res) => { return res }).catch((res) => { context.error({ statusCode: res.response.status, message: res.response.data }) }) } } </script> <style lang="scss"> .blog__overview { padding: 0 20px; max-width: 600px; margin: 40px auto 60px; p { line-height: 1.6; } } .blog__detail-link { color: #000; } </style> |
Создание папки с содержимым блога
После создания компонентов Vue.js для отображения блога нам нужно создать в Storyblok новую папку для создания страниц блога. Создайте папку en/blog и выберите для этой папки тип содержимого по умолчанию blog.
Создание статьи в блоге
Когда вы заходите в папку блога и создаете новый элемент контента, blog автоматически выбирается в качестве типа контента. Добавьте поля схемы intro (Textarea), name (Text) и body (Markdown) и создайте некоторый демонстрационный контент.
В обзоре вы должны увидеть список статей блога.
Создание карты сайта
Для создания карты сайта или дерева навигации всех страниц с помощью Nuxt.js мы будем вызывать API ссылок Storyblok. API включает отношения родитель-потомок через parent_id, и поэтому нам просто нужно сгенерировать дерево, используя вычисляемое свойство.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
<template> <section class="util__container"> <div class="sitemap"> <h1>Sitemap</h1> <div v-for="language in tree" :key="language.id"> <ul> <sitemap-item v-show="item.item.name !== 'Settings'" :model="item" v-for="item in language.children" :key="item.id"> </sitemap-item> </ul> </div> </div> </section> </template> <script> export default { data () { return { links: {} } }, computed: { tree () { let parentChilds = this.parentChildMap(this.links) return this.generateTree(0, parentChilds) } }, asyncData (context) { let version = context.query._storyblok || context.isDev ? 'draft' : 'published' return context.app.$storyapi.get('cdn/links', { version: version, starts_with: context.store.state.language, cv: context.store.state.cacheVersion }).then((res) => { return res.data }).catch((res) => { context.error(res) }) }, methods: { parentChildMap (links) { let tree = {} let linksArray = Object.keys(links).map(e => links[e]) linksArray.forEach((link) => { if (!tree[link.parent_id]) { tree[link.parent_id] = [] } tree[link.parent_id].push(link) }) return tree }, generateTree (parent, items) { let tree = {} if (items[parent]) { let result = items[parent] result.forEach((cat) => { if (!tree[cat.id]) { tree[cat.id] = {item: {}, children: []} } tree[cat.id].item = cat tree[cat.id].children = this.generateTree(cat.id, items) }) } return Object.keys(tree).map(e => tree[e]) } } } </script> <style lang="scss"> .sitemap { max-width: 600px; margin: 20px auto 60px; } </style> |
Для карты сайта в виде дерева с бесконечными узлами мы создаем компонент SitemapItem.vue и включаем его при циклическом переходе по дочерним элементам дерева.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<template> <li class="sitemap-item"> <nuxt-link :to="'/' + model.item.slug"> {{model.item.name}} </nuxt-link> <ul v-if="model.children.length > 0"> <sitemap-item :key="item.item.id" :model="item" v-for="item in model.children"> </sitemap-item> </ul> </li> </template> <script> export default { props: ['model'] } </script> <style lang="scss"> .sitemap-item { padding: 5px 0; a { color: #8ba19a; } ul { margin-top: 10px; margin-bottom: 10px; } } </style> |
Не забудьте добавить новый компонент SitemapItem в файл components.js.
1 2 3 4 5 |
... import SitemapItem from '~/components/SitemapItem.vue' ... Vue.component('sitemap-item', SitemapItem) |
В конце у нас должна получиться следующая страница.
Добавление другого языка
Со Storyblok у вас есть два варианта создания многоязычных проектов — перевод на уровне поля и перевод с несколькими деревьями. Перевод на уровне поля — хорошее решение, если большая часть контента переведена. Подумайте об использовании перевода с несколькими деревьями, если дерево содержимого отличается на каждом языке. Если вы не уверены, что выбрать, прочитайте наше руководство о i18n.
Разворачивание в реальной среде
Теперь пришло время показать ваш проект миру. Для простого, развертывание с нулевой настройкой вы можете использовать now. После того, как вы скачали и установили их настольное приложение, вы можете развернуть Nuxt.js с помощью одной команды.
1 |
now |
Вы получите уникальный URL-адрес, который затем можно будет связать через now alias со своим пользовательским доменом.
Заключение
Создать полноценный веб-сайт с помощью Nuxt.js невероятно просто, Nuxt.js обладает великолепной экосистемой. Мне очень нравится, как он абстрагирует общие задачи, которые вы обычно выполняете в конфигурации Webpack. Это немного похоже на Ruby on Rails, где соглашения идут поверх конфигурации. Для больших проектов эти соглашения облегчают подбор новых членов команды и делают проекты более удобными в обслуживании.
Источник: //dev.to
Редакция: Команда webformyself.