commit 2dd4f32646bf986a0606e833a54f65409eab3c6b Author: grigorij Date: Mon Aug 3 21:02:34 2026 +0400 first commit diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..144072b --- /dev/null +++ b/.env.local.example @@ -0,0 +1,10 @@ +# Скопируйте этот файл в .env.local и заполните реальными значениями. +# .env.local никогда не должен попадать в git (уже добавлен в .gitignore). + +# Токен вашего Telegram-бота, полученный у @BotFather +TELEGRAM_BOT_TOKEN=8539727902:AAGVuTFMrE6TOGV4PUEzCY2-MMraWqjgyvk + +# ID чата/канала, куда бот должен присылать заявки с сайта. +# Узнать свой chat_id можно, написав боту @userinfobot, +# либо получив его через метод getUpdates вашего бота. +TELEGRAM_CHAT_ID=991309145 diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ccc874 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..1b93a53 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/labnethub.iml b/.idea/labnethub.iml new file mode 100644 index 0000000..0cb38c1 --- /dev/null +++ b/.idea/labnethub.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..5555208 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5e08973 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/MACOS-SETUP.md b/MACOS-SETUP.md new file mode 100644 index 0000000..208393c --- /dev/null +++ b/MACOS-SETUP.md @@ -0,0 +1,109 @@ +# Запуск сайта LabNetHub на macOS + +## 1. Проверьте, установлен ли Node.js + +Откройте Terminal (⌘+Space → введите «Terminal» → Enter) и выполните: + +```bash +node -v +npm -v +``` + +- Если видите номер версии Node **18.17 и выше** (например, `v20.x`) — переходите к шагу 3. +- Если команда не найдена или версия ниже — переходите к шагу 2. + +## 2. Установите Node.js + +**Вариант А — официальный установщик (самый простой):** + +1. Откройте https://nodejs.org +2. Скачайте версию **LTS** +3. Запустите скачанный `.pkg`-файл и пройдите установку по шагам + +**Вариант Б — через Homebrew** (если уже пользуетесь brew): + +```bash +brew install node@20 +brew link node@20 +``` + +После установки закройте и снова откройте Terminal, затем проверьте: + +```bash +node -v +npm -v +``` + +## 3. Распакуйте архив проекта + +Дважды кликните `labnethub-website.zip` в Finder — рядом появится папка `labnethub`. + +Либо через терминал: + +```bash +cd ~/Downloads +unzip labnethub-website.zip +cd labnethub +``` + +## 4. Установите зависимости проекта + +Находясь внутри папки `labnethub`, выполните: + +```bash +npm install +``` + +Это скачает Next.js, React, Tailwind и остальные пакеты. При первом запуске занимает пару минут. + +## 5. Настройте переменные окружения + +```bash +cp .env.local.example .env.local +open -e .env.local +``` + +В открывшемся файле впишите свои значения: + +``` +TELEGRAM_BOT_TOKEN=токен_от_BotFather +TELEGRAM_CHAT_ID=ваш_chat_id +``` + +Сохраните (⌘+S) и закройте окно. + +## 6. Запустите сайт локально + +```bash +npm run dev +``` + +В терминале появится адрес — откройте в браузере: + +``` +http://localhost:3000 +``` + +Чтобы остановить сервер — нажмите `Control + C` в терминале. + +## 7. Проверка продакшен-сборки (по желанию) + +```bash +npm run build +npm run start +``` + +Так сайт запустится в том же режиме, в котором будет работать на реальном сервере. + +## Частые проблемы + +| Проблема | Решение | +|---|---| +| `command not found: npm` | Node не установлен, либо терминал открыт до установки — закройте и откройте Terminal заново | +| Порт 3000 занят | Next.js сам предложит другой порт, либо укажите вручную: `npm run dev -- -p 3001` | +| Ошибки при `npm install` | Проверьте версию Node (`node -v`) — нужна 18.17 или выше | +| `EACCES` / ошибки прав доступа | Не используйте `sudo npm install` — переустановите Node через официальный установщик или Homebrew | + +## Что дальше + +Инструкции по заполнению реальных контактов, деплою на сервер и настройке безопасности — в `README.md` в корне проекта. diff --git a/README.md b/README.md new file mode 100644 index 0000000..052fb34 --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# LabNetHub — сайт компании + +Сайт на Next.js 14 (App Router) + TypeScript + Tailwind CSS для компании +LabNetHub: разработка сайтов, ботов для соцсетей, настройка CRM, хостинг +и IT-поддержка. + +## Быстрый старт + +```bash +npm install +cp .env.local.example .env.local # заполните реальными значениями +npm run dev +``` + +Сайт откроется на http://localhost:3000 + +Сборка для продакшена: + +```bash +npm run build +npm run start +``` + +## Структура проекта + +``` +app/ — страницы (App Router) + page.tsx — Главная + websites/ — Сайты + bots/ — Боты + crm/ — CRM + hosting/ — Хостинг и серверы + support/ — IT-поддержка + contacts/ — Контакты + api/contact/ — API-роут формы обратной связи (→ Telegram) +components/ — переиспользуемые компоненты (Header, Footer, карточки и т.д.) +config/ + contacts.ts — все контакты и ссылки на соцсети в одном месте + services.ts — данные для карточек услуг и навигации +``` + +## Что нужно заполнить перед публикацией + +1. **Контакты** — откройте `config/contacts.ts` и замените placeholder-ссылки + на реальные: Telegram, VK, WhatsApp, email, город. + +2. **Telegram-уведомления формы** — в `.env.local` укажите: + - `TELEGRAM_BOT_TOKEN` — токен бота, полученный у [@BotFather](https://t.me/BotFather) + - `TELEGRAM_CHAT_ID` — ID чата/канала для приёма заявок. + Узнать свой ID можно, написав боту [@userinfobot](https://t.me/userinfobot), + либо через метод `getUpdates` вашего бота. + +3. **Домен** — в `app/layout.tsx` поле `metadataBase` укажите на ваш реальный домен. + +## Форма обратной связи + +Форма на странице `/contacts` отправляет данные на `POST /api/contact`. +Роут проверяет обязательные поля, убирает HTML-теги (защита от инъекций), +экранирует спецсимволы Markdown и пересылает сообщение в Telegram через +Bot API. В форме также есть скрытое honeypot-поле для защиты от простых ботов. + +## Безопасность + +В `next.config.mjs` включены базовые защитные HTTP-заголовки: +`Strict-Transport-Security`, `X-Frame-Options`, `X-Content-Type-Options`, +`Referrer-Policy`, `Permissions-Policy`. + +Рекомендации для сервера при деплое: + +- Раздавайте сайт только по HTTPS (например, сертификат Let's Encrypt через certbot). +- На уровне nginx настройте `limit_req` для `/api/contact`, чтобы ограничить + количество запросов от одного адреса в минуту. +- Храните `.env.local` вне репозитория — файл уже добавлен в `.gitignore`. +- Регулярно обновляйте зависимости (`npm outdated` / `npm audit`). + +## Деплой на свой сервер (LabNetHub VDS/VPS) + +Пример запуска через PM2 за nginx как обратным прокси: + +```bash +npm run build +pm2 start npm --name labnethub -- start +``` + +В конфиге nginx проксируйте запросы на `http://127.0.0.1:3000` и подключите +SSL-сертификат. При желании сайт также можно собрать в статику +(`output: "export"` в `next.config.mjs`) — тогда `/api/contact` работать не будет, +и форму нужно будет переключить на внешний сервис отправки писем. + +## Цветовая тема + +Токены дизайна заданы в `tailwind.config.ts`: + +- `base` — тёмно-синий фон (`base.950` — основной фон страницы) +- `blue` — основной акцент бренда +- `green` — акцент раздела «Хостинг и серверы» +- `ink` — оттенки текста + +Шрифты (`Unbounded`, `Manrope`, `JetBrains Mono`) подключены через +`next/font/google` в `app/layout.tsx` — при сборке скачиваются один раз +и дальше работают без обращений к Google Fonts во время выполнения. diff --git a/app/api/contact/route.ts b/app/api/contact/route.ts new file mode 100644 index 0000000..37ddc89 --- /dev/null +++ b/app/api/contact/route.ts @@ -0,0 +1,98 @@ +import { NextRequest, NextResponse } from "next/server"; + +export const runtime = "nodejs"; + +const MAX_LENGTHS = { + name: 100, + contact: 150, + message: 2000, +}; + +// Убираем HTML-теги и обрезаем длину, чтобы нельзя было сломать +// форматирование сообщения в Telegram или протащить лишний код. +function sanitize(value: unknown, maxLength: number): string { + if (typeof value !== "string") return ""; + return value.replace(/<[^>]*>/g, "").trim().slice(0, maxLength); +} + +// Экранируем спецсимволы Telegram MarkdownV2. +function escapeMarkdown(text: string): string { + return text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, (m) => `\\${m}`); +} + +export async function POST(req: NextRequest) { + const token = process.env.TELEGRAM_BOT_TOKEN; + const chatId = process.env.TELEGRAM_CHAT_ID; + + if (!token || !chatId) { + console.error("TELEGRAM_BOT_TOKEN или TELEGRAM_CHAT_ID не заданы"); + return NextResponse.json( + { ok: false, error: "Сервис временно недоступен, попробуйте позже" }, + { status: 500 } + ); + } + + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json( + { ok: false, error: "Некорректный запрос" }, + { status: 400 } + ); + } + + const { name, contact, message } = (body ?? {}) as Record; + + const cleanName = sanitize(name, MAX_LENGTHS.name); + const cleanContact = sanitize(contact, MAX_LENGTHS.contact); + const cleanMessage = sanitize(message, MAX_LENGTHS.message); + + if (!cleanName || !cleanContact || !cleanMessage) { + return NextResponse.json( + { ok: false, error: "Заполните имя, контакт и сообщение" }, + { status: 400 } + ); + } + + const text = [ + "*Новая заявка с сайта LabNetHub*", + "", + `*Имя:* ${escapeMarkdown(cleanName)}`, + `*Контакт:* ${escapeMarkdown(cleanContact)}`, + `*Сообщение:*`, + escapeMarkdown(cleanMessage), + ].join("\n"); + + try { + const telegramRes = await fetch( + `https://api.telegram.org/bot${token}/sendMessage`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + chat_id: chatId, + text, + parse_mode: "MarkdownV2", + }), + } + ); + + if (!telegramRes.ok) { + const details = await telegramRes.text(); + console.error("Telegram API error:", details); + return NextResponse.json( + { ok: false, error: "Не удалось отправить сообщение" }, + { status: 502 } + ); + } + + return NextResponse.json({ ok: true }); + } catch (err) { + console.error("Ошибка отправки в Telegram:", err); + return NextResponse.json( + { ok: false, error: "Не удалось отправить сообщение" }, + { status: 500 } + ); + } +} diff --git a/app/bots/page.tsx b/app/bots/page.tsx new file mode 100644 index 0000000..cf18382 --- /dev/null +++ b/app/bots/page.tsx @@ -0,0 +1,150 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { + Send, + MessageCircle, + ArrowUpRight, + Check, + Bell, + ShoppingBag, + Headphones, + Workflow, + Mail, + FileText, +} from "lucide-react"; +import PageHero from "@/components/PageHero"; +import SectionEyebrow from "@/components/SectionEyebrow"; +import CTASection from "@/components/CTASection"; + +export const metadata: Metadata = { + title: "Боты для Telegram и VK", + description: + "Разработка Telegram- и VK-ботов любой сложности: автоматизация заявок, уведомления, интеграция с CRM.", +}; + +const platforms = [ + { + icon: Send, + title: "Telegram-боты", + description: + "Боты любой сложности: приём заявок, уведомления, интеграция с CRM и сервисами.", + points: ["Диалоговые сценарии и кнопки", "Оплата и интеграции", "Уведомления в реальном времени"], + }, + { + icon: MessageCircle, + title: "Боты для ВКонтакте", + description: + "Боты для сообществ VK: рассылки, магазины в чате, поддержка подписчиков.", + points: ["Автоответы и сценарии диалога", "Магазин прямо в сообществе", "Рассылки по базе подписчиков"], + }, +]; + +const useCases = [ + { + icon: FileText, + title: "Приём заявок", + description: "Клиент оставляет заявку в чате — она сразу попадает в CRM или менеджеру.", + }, + { + icon: Bell, + title: "Уведомления", + description: "Автоматические напоминания клиентам и сотрудникам о статусе заказа.", + }, + { + icon: ShoppingBag, + title: "Магазин в чате", + description: "Каталог, корзина и оформление заказа без перехода на сайт.", + }, + { + icon: Headphones, + title: "Поддержка клиентов", + description: "Ответы на частые вопросы и передача сложных случаев менеджеру.", + }, + { + icon: Workflow, + title: "Интеграция с CRM", + description: "Бот пишет и читает данные напрямую из вашей CRM-системы.", + }, + { + icon: Mail, + title: "Рассылки", + description: "Массовые и сегментированные рассылки по базе клиентов.", + }, +]; + +export default function BotsPage() { + return ( + <> + + + Обсудить бота + + + + +
+
+ {platforms.map((platform) => ( +
+
+ +
+

{platform.title}

+

{platform.description}

+
    + {platform.points.map((point) => ( +
  • + + {point} +
  • + ))} +
+
+ ))} +
+
+ +
+
+ Сценарии использования +

+ Что умеют наши боты +

+ +
+ {useCases.map((item) => ( +
+
+ +
+

{item.title}

+

+ {item.description} +

+
+ ))} +
+
+
+ + + + ); +} diff --git a/app/contacts/page.tsx b/app/contacts/page.tsx new file mode 100644 index 0000000..3f5bcea --- /dev/null +++ b/app/contacts/page.tsx @@ -0,0 +1,107 @@ +import type { Metadata } from "next"; +import { Send, MessageCircle, Phone, Mail, MapPin, Clock } from "lucide-react"; +import SectionEyebrow from "@/components/SectionEyebrow"; +import NetworkGraphic from "@/components/NetworkGraphic"; +import ContactForm from "@/components/ContactForm"; +import { contacts } from "@/config/contacts"; + +export const metadata: Metadata = { + title: "Контакты", + description: "Свяжитесь с командой LabNetHub через форму, Telegram, WhatsApp или email.", +}; + +const directContacts = [ + { + icon: Send, + label: "Telegram", + value: "Написать в чат", + href: contacts.telegramBot, + }, + { + icon: Phone, + label: "WhatsApp", + value: "Написать в WhatsApp", + href: contacts.whatsapp, + }, + { + icon: Mail, + label: "Email", + value: contacts.email, + href: `mailto:${contacts.email}`, + }, + { + icon: MessageCircle, + label: "ВКонтакте", + value: "Наше сообщество", + href: contacts.vk, + }, +]; + +export default function ContactsPage() { + return ( + <> +
+
+ +
+
+ Контакты +

+ Свяжитесь с нами +

+

+ Расскажите о задаче — предложим решение и сроки. Форма ниже + приходит нам напрямую в Telegram. +

+
+
+ +
+
+
+

+ Напишите нам +

+ +
+ +
+

+ Прямые контакты +

+
+ {directContacts.map((c) => ( + +
+ +
+
+

{c.label}

+

{c.value}

+
+
+ ))} +
+ +
+
+ + {contacts.city} +
+
+ + {contacts.workingHours} +
+
+
+
+
+ + ); +} diff --git a/app/crm/page.tsx b/app/crm/page.tsx new file mode 100644 index 0000000..1612558 --- /dev/null +++ b/app/crm/page.tsx @@ -0,0 +1,143 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { ArrowUpRight, Check, Lock, Wallet, Settings2, Database } from "lucide-react"; +import PageHero from "@/components/PageHero"; +import SectionEyebrow from "@/components/SectionEyebrow"; +import CTASection from "@/components/CTASection"; + +export const metadata: Metadata = { + title: "Настройка и интеграция CRM", + description: + "Установка, настройка и интеграция CRM-систем с открытым исходным кодом под процессы вашего бизнеса.", +}; + +const benefits = [ + { + icon: Wallet, + title: "Без лицензионных платежей", + description: "Открытый исходный код — платите за настройку, а не за подписку на каждого пользователя.", + }, + { + icon: Lock, + title: "Данные остаются у вас", + description: "CRM разворачивается на вашем сервере — полный контроль над данными клиентов.", + }, + { + icon: Settings2, + title: "Гибкая настройка", + description: "Дорабатываем систему под ваши процессы, а не подстраиваем бизнес под шаблон.", + }, + { + icon: Database, + title: "Свобода интеграций", + description: "Подключаем сайт, боты, почту и другие сервисы без ограничений вендора.", + }, +]; + +const systems = [ + { + name: "EspoCRM", + description: "Гибкая CRM с удобным конструктором сущностей и процессов под любые отрасли.", + }, + { + name: "SuiteCRM", + description: "Развитая система с модулями продаж, поддержки и маркетинга из коробки.", + }, + { + name: "Odoo CRM", + description: "Часть экосистемы Odoo — легко расширяется складом, документами и бухгалтерией.", + }, +]; + +const included = [ + "Установка и настройка CRM на сервере", + "Настройка воронок продаж и полей под ваши процессы", + "Интеграция с сайтом, ботами и почтой", + "Перенос текущей базы клиентов", + "Обучение сотрудников работе с системой", + "Техническая поддержка после запуска", +]; + +export default function CrmPage() { + return ( + <> + + + Обсудить внедрение + + + + +
+
+ Почему open source +
+ {benefits.map((b) => ( +
+
+ +
+

{b.title}

+

+ {b.description} +

+
+ ))} +
+
+
+ +
+
+ С какими системами работаем +

+ Примеры CRM с открытым кодом +

+
+ {systems.map((s) => ( +
+

{s.name}

+

+ {s.description} +

+
+ ))} +
+

+ Также работаем с другими open-source CRM — подберём систему под ваши задачи и бюджет. +

+
+
+ +
+
+ Что входит в услугу +
+ {included.map((item) => ( +
+ + {item} +
+ ))} +
+
+
+ + + + ); +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..230de5e --- /dev/null +++ b/app/globals.css @@ -0,0 +1,68 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + scroll-behavior: smooth; + } + + body { + @apply bg-base-950 text-ink-100 font-body antialiased; + background-image: theme("backgroundImage.grid-fade"); + background-attachment: fixed; + } + + h1, + h2, + h3, + h4 { + @apply font-display text-ink-100; + } + + ::selection { + @apply bg-blue-600/40 text-ink-100; + } + + :focus-visible { + outline: 2px solid theme("colors.blue.400"); + outline-offset: 3px; + border-radius: 2px; + } + + ::-webkit-scrollbar { + width: 10px; + } + ::-webkit-scrollbar-track { + @apply bg-base-950; + } + ::-webkit-scrollbar-thumb { + @apply bg-base-700 rounded-full; + } + ::-webkit-scrollbar-thumb:hover { + @apply bg-blue-700; + } +} + +@layer utilities { + .text-gradient-blue { + @apply bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-blue-600; + } + .text-gradient-green { + @apply bg-clip-text text-transparent bg-gradient-to-r from-green-300 to-green-500; + } + .clip-corner { + clip-path: polygon(0 0, 100% 0, 100% calc(100% - 22px), calc(100% - 22px) 100%, 0 100%); + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + scroll-behavior: auto !important; + } +} diff --git a/app/hosting/page.tsx b/app/hosting/page.tsx new file mode 100644 index 0000000..28949d1 --- /dev/null +++ b/app/hosting/page.tsx @@ -0,0 +1,134 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { + ArrowUpRight, + Check, + MapPin, + Globe, + Gauge, + Settings2, + Cpu, +} from "lucide-react"; +import PageHero from "@/components/PageHero"; +import SectionEyebrow from "@/components/SectionEyebrow"; +import CTASection from "@/components/CTASection"; + +export const metadata: Metadata = { + title: "Хостинг и серверы", + description: + "VDS/VPS на собственных серверах в Астрахани и у партнёров по всему миру. NVMe, SLA 99.9%, защита от DDoS.", +}; + +const features = [ + { + icon: MapPin, + title: "Собственный дата-центр", + description: "Современное оборудование в нашем дата-центре в Астрахани.", + }, + { + icon: Globe, + title: "Локации по всему миру", + description: "Европа, Азия, США и другие регионы у партнёров.", + }, + { + icon: Gauge, + title: "Высокая производительность", + description: "NVMe-диски и мощные процессоры для стабильной нагрузки.", + }, + { + icon: Settings2, + title: "Гибкие конфигурации", + description: "Соберём сервер под ваши задачи, а не по шаблону.", + }, +]; + +const specs = [ + "От 1 до 32 vCPU", + "От 1 до 128 GB RAM", + "NVMe SSD", + "Linux / Windows", + "SLA 99.9%", + "Защита от DDoS", + "Резервное копирование", + "Панель управления и API", +]; + +export default function HostingPage() { + return ( + <> + + + Подобрать сервер + + + + +
+
+ {features.map((f) => ( +
+
+ +
+

{f.title}

+

+ {f.description} +

+
+ ))} +
+
+ +
+
+
+
+ +
+

+ VDS/VPS от LabNetHub +

+
+
+ {specs.map((spec) => ( +
+ + {spec} +
+ ))} +
+

+ Стоимость зависит от конфигурации и локации — рассчитаем тариф под + вашу задачу. +

+
+
+ +
+
+ Размещение проектов +

+ Размещаем сайты и боты, разработанные нашей командой, на + собственных серверах или у партнёров — без необходимости искать + хостинг отдельно и разбираться в настройке сервера самостоятельно. +

+
+
+ + + + ); +} diff --git a/app/icon.svg b/app/icon.svg new file mode 100644 index 0000000..1d801a0 --- /dev/null +++ b/app/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..967bb22 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,59 @@ +import type { Metadata } from "next"; +import { Unbounded, Manrope, JetBrains_Mono } from "next/font/google"; +import "./globals.css"; +import Header from "@/components/Header"; +import Footer from "@/components/Footer"; + +const unbounded = Unbounded({ + subsets: ["latin", "cyrillic"], + weight: ["400", "500", "600"], + variable: "--font-unbounded", + display: "swap", +}); + +const manrope = Manrope({ + subsets: ["latin", "cyrillic"], + weight: ["400", "500", "600", "700"], + variable: "--font-manrope", + display: "swap", +}); + +const jetbrainsMono = JetBrains_Mono({ + subsets: ["latin", "cyrillic"], + weight: ["400", "500"], + variable: "--font-jetbrains", + display: "swap", +}); + +export const metadata: Metadata = { + metadataBase: new URL("https://labnethub.ru"), + title: { + default: "LabNetHub — комплексные IT-решения для бизнеса", + template: "%s — LabNetHub", + }, + description: + "Сайты, боты для соцсетей, настройка CRM, хостинг и IT-поддержка. От идеи до стабильной работы вашей инфраструктуры.", + openGraph: { + title: "LabNetHub — комплексные IT-решения для бизнеса", + description: + "Сайты, боты для соцсетей, настройка CRM, хостинг и IT-поддержка.", + locale: "ru_RU", + type: "website", + }, +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + +
+
{children}
+