Привет! Я frontend-разработчик в одной компании, занимающейся электронной коммерцией.

Не буду долго рассказывать о себе, о компании и о том, как возникла потребность написать подобный проект, сразу приступлю к описанию решения.

Представим, что у вас порядка 500-1000 доменов и 5-10 разных дизайнов сайтов, распределенных между этими доменами примерно так:

  1. domain.com, domain-[city].com, domain-[subproduct]-[city].com - "сетка" 1

  2. domain2.com, domain2-[city].com, domain2-[anything]-[city].com - "сетка" 2

  3. 4,5...

Для всех сайтов нужна "одна" админка, БД разные. Для каждой "сетки" один дизайн. На каждом уникальном домене свои уникальные адреса продуктов/товаров/услуг и свои данные по продуктам. Никаких редиректов. Все страницы будем генерировать на сервере (SSG + ISR).

Из-за двух последних условий от SEOшников, вариантов реализации подобного проекта остается не так много.

Я использовал NextJS (Pages router). Так как по адресам domain.com/[firstLevel] может находиться и товар/услуга, и категория товара/услуги, и инфо страница, и любая другая страница, остается только один путь получения данных - полностью получать с бэка всю информацию по странице.

--- В папке /pages создал

  • [...slug].tsx

  • папку admin

    • [adminFoldersAndRoutes] - в реале не один динамический путь, а много папок и файлов

  • удалил index.tsx

--- В next.config.js необходимо прописать следующие настройки, чтобы получать имя хоста в getStaticProps в [...slug].tsx:

async rewrites() {
    return [
      {
        source: '/admin/:path*',
        destination: '/admin/:path*',
      },
      {
        has: [
          {
            type: 'host',
            value: '(?<host>.*)',
          },
        ],
        source: '/',
        destination: '/:host',
      },
      {
        has: [
          {
            type: 'host',
            value: '(?<host>.*)',
          },
        ],
        source: '/:path*',
        destination: '/:host/:path*',
      },
    ];
  },

--- В getStaticProps в [...slug].tsx:

Теперь в context попадает имя хоста в ctx.params.slug и весь путь страницы. С помощью React query делаем

 queryClient.prefetchQuery({
        queryFn: () => getPageData({ baseApi, url }),
        queryKey: [QUERY_KEY_FETCH_PAGE_DATA, { baseApi, url }]
      }),

baseApi определяется на основании полученного хоста.

Также в getStaticProps можно запросить domainData, где будет информация по домену (телефоны, адреса, инфо для шапки/футера, инфо домена: апи и тд...) и доставать эти данные из кэша там, где надо

В getStaticPaths:

export const getStaticPaths = async (): Promise<any> => ({
  fallback: 'blocking',
  paths: []
})

--- Передаю baseApi вниз в компонент динамической страницы (DynamicPage), там повторяю запрос на pageData, достаю данные по странице, которые содержат:

  • тип страницы (switch/case)

  • хлебные крошки

  • содержание (вывод по условиям блоков страниц)

  • СЕО

--- Различия по CSS (шрифты, цвета) делаю через <style> в DynamicPage по условиям из domainData.

--- В админке необходимо создать управление всеми сущностями, которые есть на сайтах: товары, услуги, категории товаров/услуг, страницы, пользователи, бренды и всё, всё, всё! И самое главное у всех страниц необходимо создать управление СЕО. В итоге - это в основном множество таблиц и полей форм сущностей. Админка вся на getServerSideProps и частично на CSR. На любые методы PUT/PATCH/DELETE/POST необходимо в ответ получать адреса ревалидации с бэка и отправлять на внутреннее api NextJS:

/** обработчик обновления страниц */
export default async function handler (req: NextApiRequest, res:NextApiResponse): Promise<void> {
  if (req?.method !== 'POST') {
    return res.status(405).json({ message: 'Метод не разрешен' })
  }

  try {
    if (req?.body?.secret !== process.env.REVALIDATE_TOKEN) {
      return res.status(401).json({ message: 'Неверный токен' })
    }

    if (!Array.isArray(req.body.url)) {
      return res.status(400).json({ message: 'Не правильный формат урл' })
    }

    /** промисы */
    const revalidatePromises = req?.body?.url?.map(async (url: string) => res.revalidate(url))

    /** результаты */
    const results = await Promise.allSettled(revalidatePromises)

    /** кол-во успешных */
    const successfulRevalidations = results.filter(result => result.status === 'fulfilled').length

    return res.json({ revalidated: true, successfulRevalidations })
  } catch (err) {
    return res.status(500).send('Ошибка ревалидации из хэндлера')
  }
}

Единственное неудобство - необходимость в каждый запрос передавать baseApi.

Честно говоря, до сих пор пишу и поддерживаю этот проект и ощущение, что где-то, что-то делаю не так, но все работает: страницы генерируются, регенирируются по запросу. Важные данные по цене и наличию делаю клиентскими.

Может кто-то делал что-то подобное, интересно мнение по поводу этого подхода.

Комментарии (5)


  1. jonic
    18.04.2024 21:14
    +5

    Что то дорвеи вспомнились..


    1. tacticSugar Автор
      18.04.2024 21:14

      Все примерно тоже самое. Еще работает, приносит миллионы)


  1. MarineEngineer
    18.04.2024 21:14

    Так вопрос от начинающего:

    Стили дополнительные по требованию грузятся или сразу вся пачка на всех страницах? Вообще ленивая загрузка стилей улучшила бы решение?

    Спасибо


    1. tacticSugar Автор
      18.04.2024 21:14

      Хороший вопрос, как раз занимаюсь этим. Глобальные стили и частично из компонентов (не понимаю, как это работает) сразу грузятся пачкой на любой странице ВСЕ. Смог скрыть стили разных доменов, подгружая компоненты разных доменом динамически. Смог скрыть имена классов через конфиг. Но хотелось бы полностью скрывать <style> тэги из хэдера


  1. kicumkicum
    18.04.2024 21:14

    Если baseApi это домен, то его передавать на клиент нет необходимости. Запрос вида fetch(‘/api/data’) будет происходить на текущий домен, с которого загружена страница.

    На сервере понятное дело так работать не будет, но на клиенте пожалуйста)