Данная статья предназначена в первую очередь программистам, но так же и их друзьям и знакомым.
Периодически появляется потребность написать небольшой сайт с админкой для заполнения списка товаров с фотографиями, либо срочно нужна админка к новому лэндингу, либо существующий ваш сайт требуется дополнить админкой которая еще отсутствует в расширениях 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 и проверять его на своей стороне.
