Данная статья предназначена в первую очередь программистам, но так же и их друзьям и знакомым.

Периодически появляется потребность написать небольшой сайт с админкой для заполнения списка товаров с фотографиями, либо срочно нужна админка к новому лэндингу, либо существующий ваш сайт требуется дополнить админкой которая еще отсутствует в расширениях CMS — ситуации случаются.

Если речь идет о состоявшемся бизнесе, который может позволить себе нанять специалистов и потратить 100т.р. то существует большой выбор CMS (Джумла, Модекс, Друпал...) и можно найти нужные компетенции.

Но если речь идет о стартующем бизнесе, либо семейном, либо просто о желающих начать работать самостоятельно и нужен «простой сайт», то такой бюджет будет велик, не хочется тратить деньги непонятно на что. Почему «непонятно на что»? Потому что MODX, Worldpress, Drupal и так далее «невероятно сложны» в управлении для непосвященных.
Если Вы опытный специалист, то скорее скажете — ерунда, там все элементарно! Но это вряд ли Вы так скажете: обычный пользователь, получив сайт на MODX, впадет в ступор от его Админки, впрочем как от всех других...И Тильда тоже требует обучения и понимания html сущностей, а времени на обучение нет. Нужно работающее и желательно бесплатное решение, ну или «почти бесплатное».

Раньше я отказывался от таких «бесплатных» заказов — быстро сделать простой сайт, так чтобы можно было что‑то продавать не получается — на работу уходит неделя а то и три. Но сейчас ситуация в корне поменялась. ИИ при правильном обращении генерирует сайт по любой тематике. Остается только сделать самую простую админку и передать готовый продукт клиенту буквально задаром.

И вот у меня появилась идея, на поверхности лежащая, сделать такую админку из Гугл‑таблицы. Учить ничему и никого не надо — с таблицами все работать умеют, по крайней мере ткнуть в нужную ячеку и скорректировать цену товара — это даже объяснять не надо.

Преимущества гугл таблиц:

  • элементарная простота, проще некуда;

  • позволяет работать одновременно всем интересантам;

  • позволяет оставлять коментарии и инструкции прямо в листе;

  • позволяет привлекать для консультаций разработчика, легко предоставлять и убирать доступ посторонним;

  • реализует все "хотелки" в плане администрирования данных;

  • загрузка фотографий любого вида, из папки, из интернета, из поиска и прямо с камеры;

  • админка имеет готовое мобильное приложение от Гугл :-)

Недостатки:

  • ограниченный набор записей, больше 1 000 товаров - будет сложно работать

  • минималистичность дизайна (для меня это даже плюс)

Подвернулся случай - знакомые с фуд-траком в зимний период вынуждены простаивать. Попросили как-то помочь... и здесь я опишу как я помог им получить готовый сайт, управлять которым можно с сотового телефона в любом положении. Кому интересно, читайте далее, приведу все принципиальные коды и ключевые моменты.

Краткий план:

  1. Берем хостинг, самый бюджетный, регистрируем домен.

  2. Создаем учетную запись гугл, и открываем новую гугл-таблицу.

  3. С помощью ИИ генерируем одностраничный сайт.

  4. Шаблонизируем заготовку, пишем код, который скачивает данные из гугл таблицы, пишем их в базу данных.

Выполнение:

  1. С этим пунктом все понятно, я нашел хостинг за 1400 рублей в год и оплатил сразу весь 2026. Домен в зоне ru обошелся в 200 рублей (была скидка).

  2. У ребят учетной записи в гугл не было, но это и к лучшему для начала, у меня была своя учетка, и я все сделал на ней. Самым сложным во всей этой истории было разобраться в регистрации приложения в гугл, получении сервисной учетной записи, выдаче прав... но в итоге я получил от гугла заветный файл: service_key.json.Об этом челенже можно снимать фильмы, всегда будут свежие и забавные сюжеты. Идем дальше, а с этим вопросом ИИ в помощь.

  3. Промт такой: "сгенерируй мне одностраничный сайт в одном файле на чистом html, css, javascript, посвященной нужной тематике. Сайт должен продавать небольшой ассортимент товаров, быть негромоздким и хорошо смотреться на любых устройствах"

    В ответ вы получите файл либо небольшую простынку кода.

  4. Здесь самая суть.

Для бэкэнда я выбрал 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 и проверять его на своей стороне.