| name | add-admin-panel |
| description | Добавить защищённую админ-панель к статичному HTML сайту. Netlify Identity + Netlify Blobs. Владелец может менять тексты, цены, часы, контакты через браузер без знания кода. |
| argument-hint | путь к HTML файлу сайта или название проекта |
Add Admin Panel
Добавляешь защищённую CMS-панель к статичному HTML сайту $ARGUMENTS.
Стек: Netlify Identity (авторизация) + Netlify Blobs (хранение данных) + Netlify Functions (API).
ШАГ 0 — Брифинг
Перед началом задай вопросы:
1. Какие данные должен редактировать владелец?
(тексты, цены, часы работы, контакты, описания — что именно?)
2. Сайт уже задеплоен на Netlify? Если да — название сайта?
3. Если нет — деплоим сейчас или позже?
ШАГ 1 — Структура проекта
Создай папку web-studio/sites/{project-name}/ со структурой:
{project-name}/
├── netlify.toml
├── package.json
├── public/
│ ├── index.html ← копия основного HTML
│ └── admin.html ← панель администратора
└── netlify/functions/
└── data.mjs ← API: GET/POST данных
netlify.toml
[build]
publish = "public"
functions = "netlify/functions"
package.json
{
"name": "{project-name}",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@netlify/blobs": "^8.1.0"
}
}
ШАГ 2 — API функция (netlify/functions/data.mjs)
import { getStore } from "@netlify/blobs";
const STORE_NAME = "cms";
const BLOB_KEY = "content";
async function verifyToken(authHeader) {
if (!authHeader || !authHeader.startsWith('Bearer ')) return false;
const token = authHeader.slice(7);
try {
const r = await fetch(`${process.env.URL}/.netlify/identity/user`, {
headers: { Authorization: `Bearer ${token}` }
});
return r.ok;
} catch { return false; }
}
export default async (req) => {
const store = getStore({ name: STORE_NAME, consistency: "strong" });
if (req.method === "GET") {
const data = await store.get(BLOB_KEY, { type: "json" }).catch(() => null);
return Response.json(data ?? null);
}
if (req.method === "POST") {
const ok = await verifyToken(req.headers.get('authorization'));
if (!ok) return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
let body;
try { body = await req.json(); }
catch { return new Response(JSON.stringify({ error: "Invalid JSON" }), { status: 400, headers: { "Content-Type": "application/json" } }); }
await store.setJSON(BLOB_KEY, body);
return Response.json({ ok: true });
}
return new Response("Method Not Allowed", { status: 405 });
};
export const config = { path: "/api/data" };
ШАГ 3 — Разметить HTML сайта
В public/index.html добавь:
- Перед
</head> — Netlify Identity скрипт + обработчик:
<script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
- Перед
</body> — обработчик invite-токена:
<script>
if (window.netlifyIdentity) {
window.netlifyIdentity.on('init', user => {
if (!user) {
window.netlifyIdentity.on('login', () => {
window.location.href = '/admin.html';
});
}
});
}
</script>
-
На каждый редактируемый элемент добавь id="dyn-*":
id="dyn-phone" — телефон
id="dyn-address" — адрес
id="dyn-hours" — часы работы
id="dyn-price-{service}" — цена услуги
- и т.д. — называй логично по содержимому
-
JS fetch в конце <script> — загрузка данных с API:
(async function loadCMS() {
try {
const r = await fetch('/api/data');
if (!r.ok) return;
const d = await r.json();
if (!d) return;
if (d.contacts?.phone) {
const el = document.getElementById('dyn-phone');
if (el) el.textContent = d.contacts.phone;
}
} catch(e) {}
})();
ШАГ 4 — admin.html
Структура панели:
Header: логотип + email пользователя + кнопка выйти
Tabs: по разделам (Контакты | О нас | Услуги & Цены | ...)
Body: форма для каждого таба
Save bar: фиксированный снизу — кнопка "Сохранить" + статус
Авторизация через Netlify Identity Widget:
<script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
<script>
netlifyIdentity.on('init', user => { if (user) showPanel(user); });
netlifyIdentity.on('login', user => { netlifyIdentity.close(); showPanel(user); });
netlifyIdentity.on('logout', () => showLogin());
</script>
Сохранение данных:
async function saveAll() {
const user = netlifyIdentity.currentUser();
await user.jwt(true);
const token = user.token.access_token;
const r = await fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(collectData())
});
if (r.ok) showStatus('✓ Сохранено!', 'success');
else showStatus('✗ Ошибка: ' + r.status, 'error');
}
Для таблиц цен — рендери строки динамически, каждая строка <tr> с <input> в каждой ячейке. Кнопка удалить строку + кнопка добавить строку.
ШАГ 5 — Деплой
cd web-studio/sites/{project-name}
npm install
netlify link --name {netlify-site-name}
netlify deploy --prod
После деплоя:
- Netlify → сайт → Identity → Enable
- Identity → Registration → Invite only
- Identity → Invite users → вводишь email клиента → Send
- Клиент получает письмо → Accept the invite → создаёт пароль
- Заходит на
yoursite.netlify.app/admin
Правила качества
- Дизайн admin.html должен соответствовать брендингу сайта (цвета, шрифты)
- Все поля должны иметь placeholder с текущим значением
- При загрузке панели — сразу подтягивай данные из API и заполняй форму
- Статус сохранения — зелёный успех / красная ошибка, исчезает через 4 сек
- Таблицы цен — добавить/удалить строку без перезагрузки
- На мобиле панель тоже должна работать
- Пароль НИКОГДА не хранится в коде — только через Netlify Identity