001 Добавление meta на страницу
Очень важно в рамках SEO-оптимизации для каждой страницы правильно сгенерировать передачу информации в тайтл и дескрипшн страницы. Это поможет правильно ранжировать страницу в поиске, так как поисковик выбирает ключевые слова со страницы и потом уже по ним выводит страницу.
Первым делом настроим тайтл и дескрипшн. Будем выводить в компоненте некста Head
информацию по странице. Эта информация генерируется на сервере, поэтому всё будет подставляться динамически
pages / [type] / [alias].tsx
function TopPage({ firstCategory, page, products }: TopPageProps): JSX.Element {
return (
<>
{/* данный компонент позволит перезаписать мета-информацию страницы */}
<Head>
<title>{page.metaTitle}</title>
<meta name={'description'} content={page.metaDescription} />
</Head>
<TopPageComponent firstCategory={firstCategory} page={page} products={products} />;
</>
);
}
При передаче ссылки другому человеку в интернете некоторые ресурсы позволяют из ссылки вытащить определённые элементы, которые дополнят эту ссылку полезной информацией для пользователя.
Теги для передачи:
og:image
- изображение, которое будет браться для другого сайтаog:title
- заголовокog:description
- описание
Вот пример того, что можно получить с хабра при пересылке в вк:
Далее для каждой отдельной страницы алиаса будут добавлены несколько тегов, которые позволят отобразить содержание страницы:
og:title
og:description
og:type
- позволит описать назначение страницы
pages / [type] / [alias].tsx
function TopPage({ firstCategory, page, products }: TopPageProps): JSX.Element {
return (
<>
{/* данный компонент позволит перезаписать мета-информацию страницы */}
<Head>
<title>{page.metaTitle}</title>
<meta name={'description'} content={page.metaDescription} />
<meta property={'og:title'} content={page.metaTitle} />
<meta property={'og:description'} content={page.metaDescription} />
<meta property={'og:type'} content={'article'} />
</Head>
<TopPageComponent firstCategory={firstCategory} page={page} products={products} />;
</>
);
}
Так же в компоненте страниц нужно указать мета-теги, которые будут распространятся на все страницы в приложении:
og:url
- будет генерировать в нашем случае ссылку до страницы, которая будет использоваться для перехода на наш ресурсog:locale
- скажет, что сайт построен на русском языке
pages / _app.tsx
function MyApp({ Component, pageProps, router }: AppProps): JSX.Element {
return (
<>
<Head>
<title>MyTop - наш лучший топ</title>
<link rel='icon' href='/favicon.ico' />
<link rel='preconnect' href='https://fonts.gstatic.com' />
<link
href='https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap'
rel='stylesheet'
/>
{/* сюда уже помещаем ссылку на страницу и через роутер получаем путь до открытой страницы */}
<meta
property={'og:url'}
content={process.env.NEXT_PUBLIC_DOMAIN + router.asPath}
/>
{/* тут уже просто указываем язык страницы */}
<meta property={'og:locale'} content={'ru_RU'} />
</Head>
<Component {...pageProps} />
</>
);
}
И теперь все добавленные опенграф теги можно увидеть на странице
002 Установка метрики
Первым делом нужно установить в проект зависимость яндекс-метрики
npm i react-yandex-metrika
Яндекс метрика является достаточно тяжёлым дополнительным функционалом в программу, что замедлит работу приложения в целом. Однако мы можем немного оптимизировать его работу.
Функция ym
производит подсчёт каунтера при определённых условиях. Конкретно мы будем её триггерить при изменении страницы на фронте.
Далее идёт компонент YMInitializer
который в целом инициализирует работу с метрикой.
pages / _app.tsx
import ym from 'react-yandex-metrika'; // каунтер
import { YMInitializer } from 'react-yandex-metrika'; // инициализирует работу с метрикой
function MyApp({ Component, pageProps, router }: AppProps): JSX.Element {
// далее при удачном событии изменении пути роута
router.events.on('routeChangeComplete', (url: string) => {
// проверяем, что мы не на сервере
if (typeof window !== 'undefined') {
// и тут выполняем событие hit перехода на определённый url
ym('hit', url);
}
});
return (
<>
<Head>
<title>MyTop - наш лучший топ</title>
<link rel='icon' href='/favicon.ico' />
<link rel='preconnect' href='https://fonts.gstatic.com' />
<link rel='preconnect' href='https://mc.yandex.ru' />
<link
href='https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap'
rel='stylesheet'
/>
<meta
property={'og:url'}
content={process.env.NEXT_PUBLIC_DOMAIN + router.asPath}
/>
<meta property={'og:locale'} content={'ru_RU'} />
</Head>
{/* сама инициализация яндекс-метрики */}
<YMInitializer
// массив id счётчиков метрики
accounts={[]}
// далее передаём опции
// первый параметр позволит просматривать статистику посещений, а второй откладывает метрику до загрузки приложения
options={{ webvisor: true, defer: true }}
// так же можно указать версию счётчика
version={'2'}
/>
<Component {...pageProps} />
</>
);
}
003 Husky
Husky - если говорить грубо, то это хуки для гита. Мы можем подписаться на определённые действия с гитом (на те же коммиты) и выполнять определённые действия внутри нашего проекта (например, прогон линтеров)
Устанавливаем husky:
npm i -D husky
Далее нужно установить скрипт prepare
, который установит нам хаски в проект:
"scripts": {
"prepare": "husky install",
"dev": "next dev",
"debug": "NODE_OPTIONS='--inspect' next dev",
"build": "next build",
"start": "next start",
"stylelint": "stylelint \"**/*.css\" --fix"
},
Запускаем его и получаем папку с .husky
:
npm run prepare
Далее мы можем добавить файл, который будет содержать команды, выполняемые в определённый промежуток через выполнение хаски. Конкретно тут мы добавляем процедуру, которая будет выполняться перед коммитом
npx husky add .husky/pre-commit "npm test"
Поменяем тест на выполнение команды стайллинта. И теперь перед каждым коммитом будет прогоняться стайллинт.
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run stylelint
004 Next export
Перед тем, как билдить приложение, нужно исправить несколько ошибок:
Нужно использовать роутер не полученный из приложения, а импортированный из некста
pages / _app.tsx
import Router from 'next/router';
Router.events.on('routeChangeComplete', (url: string) => {
if (typeof window !== 'undefined') {
ym('hit', url);
}
});
function MyApp({ Component, pageProps, router }: AppProps): JSX.Element {
return (
<>
<Head>
<title>MyTop - наш лучший топ</title>
<link rel='icon' href='/favicon.ico' />
<link rel='preconnect' href='https://fonts.gstatic.com' />
<link rel='preconnect' href='https://mc.yandex.ru' />
<link
href='https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap'
rel='stylesheet'
/>
<meta
property={'og:url'}
content={process.env.NEXT_PUBLIC_DOMAIN + router.asPath}
/>
<meta property={'og:locale'} content={'ru_RU'} />
</Head>
<YMInitializer accounts={[]} options={{ webvisor: true, defer: true }} version={'2'} />
<Component {...pageProps} />
</>
);
}
Далее тут нужно производить подмену мета-данных страницы только если мы получили с сервера страницы и продукты
pages / [type] / [alias].tsx
function TopPage({ firstCategory, page, products }: TopPageProps): JSX.Element {
return (
<>
{page && products && (
<>
<Head>
<title>{page.metaTitle}</title>
<meta name={'description'} content={page.metaDescription} />
<meta property={'og:title'} content={page.metaTitle} />
<meta property={'og:description'} content={page.metaDescription} />
<meta property={'og:type'} content={'article'} />
</Head>
<TopPageComponent
firstCategory={firstCategory}
page={page}
products={products}
/>
;
</>
)}
</>
);
}
Далее нужно добавить скрипт для export
из некста
"scripts": {
"prepare": "husky install",
"dev": "next dev",
"export": "next export",
"debug": "NODE_OPTIONS='--inspect' next dev",
"build": "next build",
"start": "next start",
"stylelint": "stylelint \"**/*.css\" --fix"
},
Последовательность выполнения операций:
build
- билдит приложение некста в папку.next
export
- берёт сбилженное приложение и экспортирует в папкуout
, где лежит статический полностью рабочий сайт, который можно закинуть на сервер и пользоваться им
npm run build
npm run export
Однако при выполнении команды export
мы получим ошибку, так как мы не можем пользоваться серверными возможностями некста:
- нужно убрать компонент
Image
Далее нужно будет убрать возможность производить fallback
страницы
И теперь выполняем последовательно команды:
И на выходе мы получим просто фронт-енд приложение со статическими страничками
У нас нет преимуществ NextJS:
- Нет оптимизированных изображений
- Нет фонового обновления данных
- Нет добавления новых страниц, если добавляются роуты
Итог:
Использовать такой подход можно в нескольких случаях:
- Если нам нужен просто статический сайт
- Если страницы обновляются не часто и можно делать билд под обновления
В противном случае нужно поднимать сервер некста, чтобы страницы работали со всеми фичами
005 Страницы 404, 500
Чтобы заменить страницы 404 и 500, нам нужно воспользоваться стандартными именами 404.tsx
внутри папки страниц (мы заменяем стандартный элемент ошибки)
pages / 404.tsx
import React from 'react';
import { withLayout } from '../layout/Layout';
import { Htag } from '../components';
export const Error404 = (): JSX.Element => {
return (
<>
<Htag tag={'h1'}>Ошибка 404</Htag>
</>
);
};
export default withLayout(Error404);
pages / 500.tsx
import React from 'react';
import { withLayout } from '../layout/Layout';
import { Htag } from '../components';
export const Error500 = (): JSX.Element => {
return (
<>
<Htag tag={'h1'}>Ошибка 500</Htag>
</>
);
};
export default withLayout(Error500);
Так выглядит структура страниц:
Далее тут будем выводить страницу с ошибкой, если у нас не будет пропсов продуктов и страниц
pages / [type] / [alias].tsx
function TopPage({ firstCategory, page, products }: TopPageProps): JSX.Element {
if (!page || !products) {
return <Error404 />;
}
return (
<>
<Head>
<title>{page.metaTitle}</title>
<meta name={'description'} content={page.metaDescription} />
<meta property={'og:title'} content={page.metaTitle} />
<meta property={'og:description'} content={page.metaDescription} />
<meta property={'og:type'} content={'article'} />
</Head>
<TopPageComponent firstCategory={firstCategory} page={page} products={products} />
</>
);
}
Однако нужно сказать, что у нас остаётся возможность использовать некстовские ошибки, если их импортировать:
006 Сборка контейнера Docker
Первым делом нужно создать энв-переменные для продакшена. Они не должны иметь никаких секретов и выгружаются на гит.
.env.production
Далее нам нужно запустить две данные команды:
- собирает приложение
- стартует его
npm run build
npm run start
Теперь переход по страницам происходит максимально быстро, так как они пребилжены и получили информацию со всех страниц заранее
И далее уже на этом билде можно посмотреть оптимизацию нашего приложения по лайтхаусу
И теперь мы можем запустить наше приложение внутри докера.
- Создаём в корне файл
Dockerfile
, в котором опишем настройки докера - Пишем на каком образе будет стартовать приложение:
FROM
база 14 ноды и ОС Linux Alpinenode:14-alpine
- Далее указываем рабочую директирию
WORKDIR
- подойдёт любая директирия - Далее мы добавляем внешний
package.json
в тот, что будет внутри контейнера - Далее запускаем команду установки всех зависимостей проекта
RUN
- И потом переносим всю папку приложения в докер через
ADD . .
. Работает такой переход, так как каждая команда работает как слои и все эти слои кешируются - если изменений не было от прошлых слоёв, то докер не будет перезатирать данные файлы - Далее мы устанавливаем окружение в продакшн через
ENV
- Далее нам нужно запустить билд нашего приложения
- И потом нужно через
prune
оставить только зависимости нужные для продакшн сборки приложения, чтобы не раздувать вес контейнера - далее нам нужно выполнить команду через
CMD
, чтобы запустить приложение - и в конце нужно показать один отдельный порт наружу через
Expose
- 3000
Dockerfile
FROM node:14-alpine
WORKDIR /opt/app
ADD package.json package.json
RUN npm install
ADD . .
ENV NODE_ENV production
RUN npm run build
RUN npm prune --production
CMD ["npm", "start"]
EXPOSE 3000
Далее нам нужно решить определённые ошибки по проекту, чтобы запуск приложения в контейнере не падал:
- Нужно запретить неописанное
any
- Все пути названия файлов должны быть правильными
- Импорт парсера должен происходить из
querystring
, а не изnode:querystring
Далее запускаем сборку контейнера:
docker build -t top-app-front .
И через данную команду можно посмотреть доступные образы докера, которые мы создали
docker images
007 Запуск через docker-compose
Дальше нам нужно написать настройки для докер-композа. Они пишутся на ямле и поэтому в качестве вложенности используется 2 пробела.
version
- это та версия, которую должен поддерживать докер-композservices
- это сервисы приложенияapp
- наше описываемое приложениеimage
- то изображение, которое используется для нашего приложенияcontainer_name
- имя контейнераrestart
- если приложение упадёт, то оно само перезапуститсяports
- так как контейнер закрыт от внешнего мира, ему нужно будет указать, по какому порту мы сможем попасть внутрь контейнера. Внешний порт3000
будет мапиться на внутренний порт3000
.
docker-compose.yml
version: "3"
services:
app:
image: top-app-demo
container_name: top-app-demo
restart: always
ports:
- 3000:3000
Поднимет контейнер докера по тому файлу, что находится в той же папке, где мы запустили команду
docker-compose up -d
Покажет все запущенные контейнеры
docker ps
Теперь по тому же порту будет доступно наше приложение
Так же при запуске на реальном сервере у нас поднимется и перфоманс сайта
008 Github actions
Github Actions - это пайплайны, которые позволяют автоматизировать наши процессы. Конкретно нам нужно автоматизировать сборку проекта при выгрузке его на гитхаб.
Первым делом нужно создать такую структуру пути, чтобы у нас работали наши окружения гитхаба
Далее описываем то окружение, которое нам нужно:
- name - имя нашего окружения
- on - описываем на каком действии триггерится окружение
- push - при пуше на гитхаб
- branches - выбираем ветку или ветки
- push - при пуше на гитхаб
- jobs - действия, которые будут осуществляться
- build - сборка проекта
- runs-on - запуск на какой ОС
- steps - шаги, которые нужно выполнить для действия
- uses - какую ветку используем
- name - название того экшена, который мы используем
- uses - указываем гитхаб экшена, который мы используем (можно использовать чужие экшены, а можно свои)
- with - указывает параметры выполнения экшена (у каждого экшена свои параметры)
- name - состоит из самого имени на гитхабе, имени профиля, имени репозитория и конечного названия пакета, который будет опубликован на гитхабе
- build - сборка проекта
.github / workflows / main.yml
name: Publish Docker
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
with:
registry: docker.pkg.github.com
name: docker.pkg.github.com/alaricode/top-app-demo/top-app-demo
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
tags: "develop"
Поля password
и username
задаются в секретах в самом гитхабе
При пуше на гитхаб мы увидим такую жёлтую точку, которая скажет нам, что сейчас запущен экшен
Запущенные экшены можно увидеть во вкладке с экшенами
После того, как экшен будет выполнен, мы увидим, что у нас в наличии имеется пакет с докером
И тут можно увидеть путь до докерного файла
И теперь в ямле Dockerfile
нужно указать путь до образа на гитхабе
И потом при сборке композа будет скачиваться образ