Данная статья предназначена в первую очередь программистам, но так же и их друзьям и знакомым.
Периодически появляется потребность написать небольшой сайт с админкой для заполнения списка товаров с фотографиями, либо срочно нужна админка к новому лэндингу, либо существующий ваш сайт требуется дополнить админкой которая еще отсутствует в расширениях CMS — ситуации случаются.
Если речь идет о состоявшемся бизнесе, который может позволить себе нанять специалистов и потратить 100т.р. то существует большой выбор CMS (Джумла, Модекс, Друпал...) и можно найти нужные компетенции.
Но если речь идет о стартующем бизнесе, либо семейном, либо просто о желающих начать работать самостоятельно и нужен «простой сайт», то такой бюджет будет велик, не хочется тратить деньги непонятно на что. Почему «непонятно на что»? Потому что MODX, Worldpress, Drupal и так далее «невероятно сложны» в управлении для непосвященных.
Если Вы опытный специалист, то скорее скажете — ерунда, там все элементарно! Но это вряд ли Вы так скажете: обычный пользователь, получив сайт на MODX, впадет в ступор от его Админки, впр��чем как от всех других...И Тильда тоже требует обучения и понимания html сущностей, а времени на обучение нет. Нужно работающее и желательно бесплатное решение, ну или «почти бесплатное».
Раньше я отказывался от таких «бесплатных» заказов — быстро сделать простой сайт, так чтобы можно было что‑то продавать не получается — на работу уходит неделя а то и три. Но сейчас ситуация в корне поменялась. ИИ при правильном обращении генерирует сайт по любой тематике. Остается только сделать самую простую админку и передать готовый продукт клиенту буквально задаром.
И вот у меня появилась идея, на поверхности лежащая, сделать такую админку из Гугл‑таблицы. Учить ничему и никого не надо — с таблицами все работать умеют, по крайней мере ткнуть в нужную ячеку и скорректировать цену товара — это даже объяснять не надо.
Преимущества гугл таблиц:
элементарная простота, проще некуда;
позволяет работать одновременно всем интересантам;
позволяет оставлять коментарии и инструкции прямо в листе;
позволяет привлекать для консультаций разработчика, легко предоставлять и убирать доступ посторонним;
реализует все "хотелки" в плане администрирования данных;
загрузка фотографий любого вида, из папки, из интернета, из поиска и прямо с камеры;
админка имеет готовое мобильное приложение от Гугл :-)
Недостатки:
ограниченный набор записей, больше 1 000 товаров - будет сложно работать
минималистичность дизайна (для меня это даже плюс)
Подвернулся случай - знакомые с фуд-траком в зимний период вынуждены простаивать. Попросили как-то помочь... и здесь я опишу как я помог им получить готовый сайт, управлять которым можно с сотового телефона в любом положении. Кому интересно, читайте далее, приведу все принципиальные коды и ключевые моменты.
Краткий план:
Берем хостинг, самый бюджетный, регистрируем домен.
Создаем учетную запись гугл, и открываем новую гугл-таблицу.
С помощью ИИ генерируем одностраничный сайт.
Шаблонизируем заготовку, пишем код, который скачивает данные из гугл таблицы, пишем их в базу данных.
Выполнение:
С этим пунктом все понятно, я нашел хостинг за 1400 рублей в год и оплатил сразу весь 2026. Домен в зоне ru обошелся в 200 рублей (была скидка).
У ребят учетной записи в гугл не было, но это и к лучшему для начала, у меня была своя учетка, и я все сделал на ней. Самым сложным во всей этой истории было разобраться в регистрации приложения в гугл, получении сервисной учетной записи, выдаче прав... но в итоге я получил от гугла заветный файл:
service_key.json.Об этом челенже можно снимать фильмы, всегда будут свежие и забавные сюжеты. Идем дальше, а с этим вопросом ИИ в помощь.Промт такой: "сгенерируй мне одностраничный сайт в одном файле на чистом html, css, javascript, посвященной нужной тематике. Сайт должен продавать небольшой ассортимент товаров, быть негромоздким и хорошо смотреться на любых устройствах"
В ответ вы получите файл либо небольшую простынку кода.
Здесь самая суть.
Для бэкэнда я выбрал laravel, поскольку часто им пользуюсь, но можно писать в чистом php, вообще на чем угодно, где есть библиотека Google apiclient. Если вы пишете на Питоне или nodejs, то рассматривайте текст ниже, как псевдокод - методы google apiclient не поменяются.
Но сначала на листе Гугл таблицы, который назовем Price, с некоторым пропуском верхних строк, наберем несколько товарных позиций, например
Артикул Название Цена Описание Фото Активно art1 ... art2 ...
Инициатором обмена данными будет пользователь, который редактирует таблицу, правит цены и прочее. Ему нужно дать кнопку "Отправить". В гугл таблицах появилась новая сущность - кнопки-рисунки, которым можно назначить выполнение скрипта, но я решил не усложнять. Просто над данными размес��ил в ячейке такую функцию
=ГИПЕРССЫЛКА(СЦЕПИТЬ("https://mysite.ru/google-price?"; "range=A4:F"); "Выгрузить все данные (кроме фото)")
mysite.ru это и есть наш сайт, в ячейке будет показана Гиперссылка, при клике по которой браузер откроет соседнюю вкладку, и мы сможем показать в этой вкладке лог выгрузки данных, вывести информацию об ошибках, то есть дать обратную связь, что крайне важно для пользователя.
Если решитесь повторить, рекомендую заменить читаемый адрес на сложноподбираемый, например mysite.ru/google-hjhgs4543nnsds, для безопасности.
Со стороны нашего сайта пишем роут, который принимает обработку запроса https://mysite.ru/google-price?range=A4:F
Мои данные начинались с четвертой стрки поэтому именно такой диапазон я указываю в ссылке.
На стороне сайта через composer подключаем библиотеку google/apiclient
composer require google/apiclient
Напишем сервис-класс, который будет прятать в себе взаимодействие сайта с гуглом
namespace App\GoogleApi; use Google\Client as GooClient; use Google\Service\Sheets; // из адреса url гугл-таблицы берем ее ID, то что после spreadsheets/d/ define('SPREADSHEET_ID', '1QBLAblaBlajsdfsdflsdlkjlkjlkjlk_yE4'); class GoogleService { protected $service; public function __construct() { $client = new GooClient(); $googleAccountKeyFilePath = app()->basePath('app/GoogleApi/service_key.json'); putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $googleAccountKeyFilePath); $client->useApplicationDefaultCredentials(); $client->addScope('https://www.googleapis.com/auth/spreadsheets'); $client->addScope('https://www.googleapis.com/auth/drive'); // если вы не пользуйтесь сервис аккаунтом гугла, а будете от имени пользователя // вызывать апи, то также потребуется настройка редиректа для подтверждения // пользователем прав доступа к данным. В моем случае я использую service account // и следующие две строчки не нужны //$redirect_uri = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF']; //$client->setRedirectUri($redirect_uri); $this->service = new Sheets($client); } function loadprice($range) { $responseData = $this->service->spreadsheets_values->get(SPREADSHEET_ID, $range); $rows = $responseData->getValues(); $products = []; foreach ($rows as $index => $row) { $products[] = [ 'sku' => $row[0], 'name' => $row[1], 'price' => str_replace(',', '.', $row[2]), 'description' => $row[3], 'active' => filter_var($row[5], FILTER_VALIDATE_BOOLEAN) ]; } return $products; } } // и теперь где-то в другом месте, в коде контроллера, // обрабатывающего запрос на выгрузку данных можно написать use App\GoogleApi\GoogleService; // ... // создаем экземпляр нашего сервис класса. $service = new GoogleService(); // получаем массив строк данных c листа 'Price' нашей Гугл таблицы. // Очень важно указать корректный и полный диапазон данных с указанием листа // $request - это объект запроса, в laravel попадает в контроллер как параметр // функции контроллера, здесь имеется в виду $range = $_GET['range'] $range = $request->string('range'); // загружаем данные :) $prods = $service->loadprice("Price!" . $range); // у меня есть модель Product c настроенными кастингами, суть такая, что // при массовом обновлении провести необходимые преобразования: // даты в строки нужного формата, для float ',' меняем на '.' // или чекбоксы в 0/1 или список строк сначала в массивы а потом в json // потому что в Mysql и Google таблицах форматы на совпадают. // В этом месте мы обновляем или создаем новые товары в базе данных. $updata = collect($prods)->mapinto(Product::class); $res = Product::upsert($updata->toArray(), uniqueBy:['sku'], update:['name', 'price', 'description', 'active']);
Для продающего сайта очень важно иметь качествыенные и свои, не из интернета, фотографии продукции. Поэтому без добавления и выгрузки фотографий игра свечь не стоит, но гугл таблицы умеют вставлять изображения в ячейки! А вот выгружать их надо иначе.
При получении данных с листа как указано выше, применяется объект spreadsheets_values, который отправляет в нашу сторону только данные в виде двумерного массива. Это очень компактный способ обмена, позволяющий выгружать сразу все записи. Содержимое ячеек с изображением не передается, мы получим пустую строку. Но нам и не требуется выгружать все изображения. Пользователь будет выгружать изображения по мере их добавления в ячейки столбца "Фото".
Важно (!), чтобы изображения были вставлены, как Image In Cell (Изображение в ячейке).
Механизм отправки тот же, через ссылку, но этого будет мало. В Google Spreadshet api нет методов получения каких-либо данных об изображении, потому что они хранятся отдельно от данных листа. Проверив разные варианты, я остановился на самом локаничном способе - через App Script. App Script это привязанное к таблице Приложение на javascript, имеющее доступ к ячейкам листа и неограниченный функционал работы с данными.
Итак, в Гугл Таблице кликаем в меню Расширения | App Script, в соседней вкладке откроется окно приложения. На вертикально вкладке Код js набираем 2 функции:
function getCellImageData(CellAdress) { var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); var range = sheet.getRange(CellAdress); var value = range.getValue(); if (value && typeof value === 'object' && value.toString() === "CellImage") { return value.getContentUrl(); } } function doGet(e) { const cell = e.parameter.cell; // Получаем параметр ?cell=E4 из URL const result = getCellImageData(cell); return ContentService.createTextOutput(result); }
Функция getCellImageData принимает параметром адрес ячейки с изображением и возвращает URL для его скачивания. Важная деталь - getActiveSpreadsheet().getActiveSheet() вернет автивную книгу и активную вкладку открытого в браузере листа, а нам это и надо. Поскольку инициатором обмена является пользователь, кликающий по ссылке, то активной вкладкой будет именно Price. Это не баг, это фича - элемент защиты от ботов. Нельзя инициировать обмен фото, не имея открытой таблицы.
Вторая функция doGet выполнит обработку исходящего от нас GET запроса с параметрами. Но для того, чтобы такой запрос принимался, App Script надо опубликовать!
Для этого справа вверху на странице App Script Приложения есть внопка "Начать развертывание". Интуиция подскажет что жать, но для начала:
выберите вариант "Новое развертывание",
далее "выберите тип" Web Приложение,
Заполните поля, доступ нужно дать "всем",
завершите развертывание кнопкой "Начать развертывание".
Разрешение нужно дать "всем" - не бойтесь, ссылка на приложение будет только у вас и ее невозможно подобрать.
Развертывание в результатах своей работы покажет вам ссылку на Веб-приложение, завершающуюся на /exec.
Копируем эту ссылку и возвращаемся в наш код класса GoogleService.
Определим новую константу, в которую вставляем полученный URL.
define('IMAGE_EXEC', 'https://script.google.com/macros/s/<id_вашего_app_script>/exec'); // и добавим еще один метод класса function loadphoto($cell) { // я использую встроенный http-client laravel но можно и через curl $response = Http::get(IMAGE_EXEC . "?cell=$cell"); // получаем от app script временный url изображения для скачивания $imageurl = $response->body(); // скачиваем изображение по URL (downloadImageFromUrl пишем самостоятельно) $imagepath = $this->downloadImageFromUrl($imageurl); return $imagepath; }
Что здесь происходит: функция принимает параметром адрес ячейки, добавляет ее как параметр к запросу в адрес app script приложения и в ответ получает url изображения для скачивания.
Внимание, это временный url, его нельзя сохранять в базе данных. Поэтому мы вызываем функцию downloadImageFromUrl($imageurl) которая скачает изображение, уменьшит его если надо, создаст тамбнейлы, сделает resize и crop, все что вы сами захотите, поэтому я не буду приводить здесь ее текст.
Но это не все, нам нужен эндпоинт чтобы принять запрос от пользователя после вставки изображения в ячейку таблицы.
// в нашем контроллере отклик на url //https://mysite.ru/google-photo?range=E4&sku=sku1 public function photo(Request $request){ $service = new GoogleService(); $sku = $request->string('sku'); $prod = Product::where('sku', $sku)->first(); if (!$prod) { // выгружаем фото, а продукта еще нет? die "Перед выгрузкой фото, выгрузите данные"; } $range = $request->string('range'); $imagepath = $service->loadphoto($range); //сохраняем путь к изображению в базе //код в стиле ORM, у меня Eloquent $prod->photo = $imagepath; $prod->save(); //сообщаем пользователю об успехе echo "Картинка артикула $sku успешно выгружена"; }
Осталось только разместить в ячейках прейскуранта соответствующую ссылки. По одной в каждой строке товара. Заполним вручную ссылку для первой строки и скопируем протяжкой вниз во все другие ячейки:
=ГИПЕРССЫЛКА(СЦЕПИТЬ("mysite.ru/google-photo?"; "range=J"; СТРОКА(); "&sku="; A4); "Выгрузить фото")
мои данные начинаются в четвертой строке и такую ссылку я вставляю в самую правую ячейку 4-ой строки. После загрузки изображения пользователь кликает по этой ссылке справа и изображение выгрузится на сайт.
Итоги:
Первый день ушел на подготовку: регистрация домена, хостинг, сервисная учетка Гугл, ожидание сертификатов и обновления dns. Второй день - написание кода. Третий день - на причесывание, улучшение, написание этой статьи.
Уверен, что пользователь тоже будет доволен понятным, лаконичным, хотя и кандовым инструментом-админкой, а также ценой вопроса.
В качетсве бартерного вознаграждения я попросил кусок премиального брискета весом в 4 кг. Меня устроило.
Развитие:
В итоге по просьбе товарищей я добавил в админку еще несколько листов для управления рекламными акциями, скидками, промокодами и прочим. Сама по себе Гугл таблица может многое и все вопросы по ее интефейсу есть в том же гугле, уверен, что дергать вопросами меня не будут. Обратный обмен данными работает также - на сайт идет запрос, в ответ отправляются данные для отчетов и прочее и прочее. Можно работать, если нет капризов и претензий к бантикам интерфейса.
Рекомендации:
В целях безопасности сделайте эндпоинты случайным набором символов. Тогда ваша админка будет защищена так же, как вы защищаете контент этой гугл таблицы. Можно вместо этого добавить к ссылке token и проверять его на своей стороне.
