Automatyzacja

Jeden formularz. Pięć automatyzacji. Zero pracy ręcznej.

Analiza systemowa
Jeden formularz. Pięć automatyzacji. Zero pracy ręcznej.

Każda szkolna strona internetowa ma ten sam problem: treść tkwi w czyjejś głowie, a nie na stronie. Nauczyciele nie publikują, bo publikowanie jest zbyt trudne — zły folder, zły format nazwy pliku, brakujące zdjęcie, zapomniana data.

Odpowiedzią nie jest bardziej rozbudowany edytor. Odpowiedzią jest usunięcie edytora w całości i zastąpienie go czymś, co zna już każdy nauczyciel: formularzem Google Forms.

Dlaczego formularz bije CMS na głowę

Typowy „szkolny CMS” mnoży opory na każdym kroku: zaloguj się, znajdź właściwą sekcję, przypomnij sobie format, wgraj zdjęcie osobno, kliknij publikuj i módl się, żeby nic się nie posypało. Nauczyciele go unikają. Treść umiera w skrzynkach mailowych.

Google Forms usuwa to wszystko. Pięć pól. Trzydzieści sekund. Gotowe.

Najlepszy interfejs redakcyjny to taki, który w ogóle nie wygląda jak interfejs redakcyjny.

Jest znajomy z definicji. Każdy nauczyciel wypełniał kiedyś Google Forms. Żadnych szkoleń, żadnej dokumentacji, żadnego wdrożenia.

Działa z telefonu. Nauczyciel fotografuje szkolny koncert, otwiera formularz w drodze do samochodu, wpisuje kilka słów kluczowych — i wpis jest już opublikowany, zanim ten wróci do domu.

Wymusza strukturę. Nie ma ryzyka pominięcia daty, źle sformatowanego tytułu czy wrzucenia pliku do niewłaściwego folderu. Pola formularza umową między nauczycielem a systemem.

Jest kontrolowany i audytowalny. Ograniczasz go do swojej domeny. Każde zgłoszenie niesie znacznik czasu i autora. Pełna historia, zero narzutu.

Potok: co dzieje się po kliknięciu „Wyślij”

Nauczyciel widzi formularz, a potem widzi efekt na szkolnej stronie. Między tymi dwoma momentami Apps Script uruchamia pięcioetapowy potok — niewidoczny, automatyczny i odporny na błędy.

Google Form
    ↓ trigger: onFormSubmit
Apps Script

┌───────────────────┬────────────────────┐
│                   │                    │
Gemini API      Google Drive         Google Calendar
(expands notes) (creates Doc)       (creates event)
│                   │
└───────────────────┘

    Web App CMS

    Google Sites

Nauczyciel robi jedną rzecz. System robi pięć.

Formularz: pięć pól, żadnych instrukcji

Formularz ma dwie kategorie pól. Pola treści trafiają do opublikowanego dokumentu i na stronę. Pola operacyjne sterują tym, jak Apps Script kieruje zgłoszeniem — nigdy nie pojawiają się w gotowym artykule.

Pola treści:

  • Tytuł — krótka odpowiedź. Przykład: „Rodzinny Koncert Bożonarodzeniowy”.
  • Data — wybór daty. Jeśli wiadomość nie jest związana z konkretnym wydarzeniem, nauczyciel wpisuje datę publikacji.
  • Typ — lista rozwijana: CONCERT, INFO, PROJECT, SPORT, RECRUITMENT. Staje się plakietką na stronie.
  • Słowa kluczowe / opis — akapit. Nauczyciel spisuje sedno: „Klasy 4–6 zaśpiewały kolędy. Obecni byli rodzice i goście. Miejsce: Pałac Młodzieży.” Nie musi pisać dopracowanej prozy — Gemini to rozwinie.
  • Zdjęcia — przesyłanie plików, tylko obrazy, dozwolone wiele plików.

Pola operacyjne (dodaj według potrzeb swojej szkoły): docelowe klasy, priorytet, kanały publikacji.

Krok po kroku: co robi skrypt

Krok 1 — Odczyt odpowiedzi z formularza

Apps Script przechwytuje zdarzenie onFormSubmit i odczytuje każde pole po kolei za pomocą responses[N].getResponse(). Data jest formatowana do postaci YYYY-MM-DD — formatu, którego Web App CMS oczekuje w nazwie pliku.

Krok 2 — Gemini rozwija słowa kluczowe

Punkty spisane przez nauczyciela trafiają do Gemini 2.5 Flash z rygorystycznym promptem: napisz 4–5 zdań, neutralnym, informacyjnym tonem, bez zmyślonych faktów. Oryginalne słowa kluczowe są zachowane na dole dokumentu jako metadane — dzięki temu zawsze widać, co napisał człowiek, a co rozwinęła AI.

Jeśli Gemini jest niedostępny (limit zapytań, awaria), tekstem artykułu staje się oryginalny opis. Potok nigdy nie blokuje się na etapie AI.

Krok 3 — Utworzenie ustrukturyzowanego Google Docs

W folderze CMS powstaje nowy dokument o nazwie YYYY-MM-DD | Title. Apps Script wstawia: nagłówek H1, plakietkę typu (pogrubioną, kolorową), tekst z Gemini, przeskalowane zdjęcia (maks. 5, maks. 500px szerokości) oraz oryginalne słowa kluczowe jako szare metadane 9pt na dole. Plik zostaje udostępniony w trybie „każdy z linkiem może wyświetlać”.

Krok 4 — Dodanie do szkolnego kalendarza

Powstaje całodniowe wydarzenie w kalendarzu z tagiem typu wydarzenia i linkiem do dokumentu. Dyrekcja, nauczyciele i rodzice mogą zasubskrybować kalendarz i automatycznie widzieć każdą opublikowaną wiadomość.

Krok 5 — Unieważnienie cache CMS

Klucz cache skryptu używany przez Web App zostaje wyczyszczony. Kolejne wczytanie strony pobiera świeżą listę dokumentów — nowy wpis pojawia się bez żadnego ręcznego odświeżania czy ponownej publikacji.

Kompletny skrypt

Podepnij go do swojego Google Forms przez Script Editor (⋮ → Script editor). Klucz API Gemini przechowuj w Project Settings → Script Properties jako GEMINI_API_KEY.

// ===== CONFIG =====
const CONFIG = {
  FOLDER_ID:    "PASTE_YOUR_NEWS_FOLDER_ID",
  CALENDAR_ID:  "PASTE_YOUR_CALENDAR_ID",
  FONT_FAMILY:  "Lato",
  MAX_IMAGES:   5,
  MAX_IMG_WIDTH: 500,
  CACHE_KEY:    "NEWS_HTML"
};

// ===== TRIGGER =====
function onFormSubmit(e) {
  const responses = e.response.getItemResponses();
  const title    = responses[0].getResponse();
  const dateStr  = responses[1].getResponse();
  const type     = responses[2].getResponse();
  const keywords = responses[3].getResponse();
  const fileIds  = responses[4] ? responses[4].getResponse() : null;

  const date    = new Date(dateStr);
  const isoDate = Utilities.formatDate(date, "Europe/Warsaw", "yyyy-MM-dd");

  let newsText = generateNewsText(title, type, keywords);
  if (!newsText) newsText = keywords;

  const docUrl = createNewsDoc(title, isoDate, type, newsText, keywords, fileIds);
  createCalendarEvent(title, date, type, docUrl);
  clearCmsCache();
}

// ===== GEMINI =====
function generateNewsText(title, type, keywords) {
  try {
    const apiKey = PropertiesService.getScriptProperties()
      .getProperty("GEMINI_API_KEY");
    if (!apiKey) return "";

    const prompt =
      "You are a school website editor. Based on the teacher's notes, " +
      "write a polished news post. 4-5 sentences, neutral informational " +
      "tone, understandable for parents. Do not invent facts.\n\n" +
      "Title: " + title + "\nEvent type: " + type +
      "\nTeacher's notes: " + keywords;

    const url =
      "https://generativelanguage.googleapis.com" +
      "/v1beta/models/gemini-2.5-flash:generateContent?key=" + apiKey;

    const payload = {
      contents: [{ parts: [{ text: prompt }] }],
      generationConfig: {
        maxOutputTokens: 1024,
        thinkingConfig: { thinkingBudget: 0 }
      }
    };

    const res = UrlFetchApp.fetch(url, {
      method: "post",
      contentType: "application/json",
      payload: JSON.stringify(payload),
      muteHttpExceptions: true
    });

    const json = JSON.parse(res.getContentText());
    if (json.candidates?.[0]) {
      return json.candidates[0].content.parts[0].text.trim();
    }
  } catch (err) {
    Logger.log("Gemini error: " + err);
  }
  return "";
}

// ===== DOCUMENT =====
function createNewsDoc(title, isoDate, type, newsText, keywords, fileIds) {
  const folder = DriveApp.getFolderById(CONFIG.FOLDER_ID);
  const fileName = isoDate + " | " + title;
  const doc  = DocumentApp.create(fileName);
  const body = doc.getBody();

  const h1 = body.getParagraphs()[0];
  h1.setHeading(DocumentApp.ParagraphHeading.HEADING1);
  h1.setText(title);
  h1.setFontFamily(CONFIG.FONT_FAMILY);

  if (type) {
    const badge = body.appendParagraph(type.toUpperCase());
    badge.setBold(true).setForegroundColor("#1a73e8").setFontFamily(CONFIG.FONT_FAMILY);
  }

  body.appendParagraph("");
  body.appendParagraph(newsText).setFontFamily(CONFIG.FONT_FAMILY);

  if (fileIds?.length > 0) {
    body.appendParagraph("");
    const limit = Math.min(fileIds.length, CONFIG.MAX_IMAGES);
    for (let i = 0; i < limit; i++) {
      try {
        const blob  = DriveApp.getFileById(fileIds[i]).getBlob();
        const image = body.appendImage(blob);
        const w = image.getWidth();
        if (w > CONFIG.MAX_IMG_WIDTH) {
          const ratio = CONFIG.MAX_IMG_WIDTH / w;
          image.setWidth(CONFIG.MAX_IMG_WIDTH);
          image.setHeight(Math.round(image.getHeight() * ratio));
        }
        body.appendParagraph("");
      } catch (err) { Logger.log(err); }
    }
    if (fileIds.length > CONFIG.MAX_IMAGES) {
      const info = body.appendParagraph(
        "Remaining photos (" + (fileIds.length - CONFIG.MAX_IMAGES) + ") available in Drive folder."
      );
      info.setFontSize(9).setForegroundColor("#999999").setItalic(true);
    }
  }

  const kw = body.appendParagraph("Keywords: " + keywords);
  kw.setFontSize(9).setForegroundColor("#999999").setItalic(true).setFontFamily(CONFIG.FONT_FAMILY);

  const file = DriveApp.getFileById(doc.getId());
  folder.addFile(file);
  DriveApp.getRootFolder().removeFile(file);
  file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);

  return doc.getUrl();
}

// ===== CALENDAR =====
function createCalendarEvent(title, date, type, docUrl) {
  try {
    const cal = CalendarApp.getCalendarById(CONFIG.CALENDAR_ID);
    if (!cal) { Logger.log("Calendar not found"); return; }
    const event = cal.createAllDayEvent(
      (type ? "[" + type + "] " : "") + title, date
    );
    event.setDescription("School news\n\nDocument:\n" + docUrl);
  } catch (err) {
    Logger.log("Calendar error: " + err);
  }
}

// ===== CACHE =====
function clearCmsCache() {
  try {
    CacheService.getScriptCache().remove(CONFIG.CACHE_KEY);
  } catch (err) {
    Logger.log("Cache clear skipped: " + err);
  }
}

Lista kontrolna wdrożenia

Konfiguracja Google Forms:

  • Utwórz nowy Google Forms ograniczony do domeny Twojej szkoły
  • Dodaj pole: Tytuł (krótka odpowiedź, wymagane)
  • Dodaj pole: Data (wybór daty, wymagane)
  • Dodaj pole: Typ (lista rozwijana: CONCERT, INFO, PROJECT, SPORT, RECRUITMENT)
  • Dodaj pole: Słowa kluczowe / opis (akapit, wymagane)
  • Dodaj pole: Zdjęcia (przesyłanie plików, tylko obrazy, zezwól na wiele)

Konfiguracja Apps Script:

  • Otwórz Script Editor (⋮ → Script editor) i wklej cały skrypt
  • Zaktualizuj CONFIG.FOLDER_ID identyfikatorem swojego folderu CMS (z adresu URL w Drive)
  • Zaktualizuj CONFIG.CALENDAR_ID identyfikatorem szkolnego kalendarza
  • Dodaj GEMINI_API_KEY w Project Settings → Script Properties

Triggery i testowanie:

  • Ustaw trigger: ikona zegara → Add trigger → onFormSubmit → On form submit
  • Wyślij testowe zgłoszenie i sprawdź: dokument pojawia się w folderze, wydarzenie w kalendarzu utworzone, strona się aktualizuje

Wersja interaktywna

Poniższy widżet przeprowadza przez cały potok krok po kroku i pozwala poznać kod w kontekście.

📋
Google Form
⚙️
Apps Script
🧠
Gemini API
📄
Google Docs
📅
Calendar + Sites
📋
1. Teacher fills the form
5 fields · 30 seconds

The form has two categories of fields. Content fields (title, date, type, keywords, photos) flow into the published document. Operational fields (target classes, priority) control routing. Teachers never touch a folder, filename, or Docs template.

⚙️
2. onFormSubmit trigger fires
Event caught · pipeline starts

Apps Script catches the form submit event and reads each field in order using responses[N].getResponse(). The date is formatted to YYYY-MM-DD — the format the CMS Web App expects in the filename. Each downstream step is independent: if one fails, the others continue.

Apps Script
function onFormSubmit(e) {
  const responses = e.response.getItemResponses();
  const title    = responses[0].getResponse();
  const dateStr  = responses[1].getResponse();
  const type     = responses[2].getResponse();
  const keywords = responses[3].getResponse();
  const fileIds  = responses[4]
                   ? responses[4].getResponse() : null;
  const isoDate = Utilities.formatDate(
    new Date(dateStr), "Europe/Warsaw", "yyyy-MM-dd"
  );
  // → hand off to next steps
}
🧠
3. Gemini expands the keywords
Bullet points → 4–5 sentence paragraph

The teacher's notes are sent to Gemini 2.5 Flash with a strict prompt: neutral tone, no invented facts, 4–5 sentences. thinkingBudget: 0 ensures all tokens go to the article text. If Gemini is unavailable (rate limit, outage), the original keywords become the article. The pipeline never blocks on the AI step.

Apps Script
const prompt =
  "You are a school website editor. Write a polished " +
  "news post. 4-5 sentences, neutral tone, " +
  "understandable for parents. No invented facts.\n\n" +
  "Title: " + title + "\nType: " + type +
  "\nNotes: " + keywords;

const payload = {
  contents: [{ parts: [{ text: prompt }] }],
  generationConfig: {
    maxOutputTokens: 1024,
    thinkingConfig: { thinkingBudget: 0 }
  }
};
📄
4. Structured document created
Filename · H1 · badge · text · photos · metadata

A new Doc is created in the CMS folder with filename YYYY-MM-DD | Title. Apps Script inserts: H1 heading, coloured type badge, Gemini text, scaled photos (max 5, max 500px wide), and the original keywords as 9pt grey metadata at the bottom. The file is shared as 'anyone with link can view.'

Apps Script
const fileName = isoDate + " | " + title;
const doc  = DocumentApp.create(fileName);
const body = doc.getBody();

// H1 title
body.getParagraphs()[0]
  .setHeading(DocumentApp.ParagraphHeading.HEADING1)
  .setText(title);

// Type badge
body.appendParagraph(type.toUpperCase())
  .setBold(true).setForegroundColor("#1a73e8");

// AI-generated article text
body.appendParagraph(""); 
body.appendParagraph(newsText);

// Move to CMS folder, share publicly
const file = DriveApp.getFileById(doc.getId());
folder.addFile(file);
DriveApp.getRootFolder().removeFile(file);
file.setSharing(
  DriveApp.Access.ANYONE_WITH_LINK,
  DriveApp.Permission.VIEW
);
📅
5. Published & synced
Calendar event · cache cleared · page live

An all-day calendar event is created with the event type tag and a link to the document. The script cache key used by the Web App is cleared — the next page load fetches the fresh document list. The post is live on the school website without any manual action.

Apps Script
// Calendar event
const event = cal.createAllDayEvent(
  "[" + type + "] " + title, date
);
event.setDescription("Document:\n" + docUrl);

// Invalidate CMS page cache
CacheService.getScriptCache()
  .remove(CONFIG.CACHE_KEY);

Ten sam wzorzec, różne problemy

Ta architektura — formularz → Apps Script → ustrukturyzowany rezultat — nadaje się do wielokrotnego użycia w dziesiątkach szkolnych procesów.

Zapisy na wycieczki. Nauczyciel przesyła nazwę wydarzenia, datę, klasę i liczbę uczestników. Skrypt tworzy dokument zapisów z szablonu, dodaje wpis do kalendarza wycieczek, wysyła maila do nauczyciela i powiadamia dyrekcję.

Raporty ewaluacyjne. Po zamknięciu ankiety Apps Script zlicza odpowiedzi, wysyła je do Gemini po streszczenie, generuje plik PDF i umieszcza go w folderze Drive dyrekcji. To, co kiedyś zajmowało godzinę, dzieje się w kilka sekund.

Cotygodniowy newsletter dla rodziców. Trigger czasowy zbiera dokumenty z wiadomościami z ostatnich siedmiu dni, prosi Gemini o napisanie przeglądu i wysyła maila w kopii ukrytej (BCC) do wszystkich grup rodziców. Zero wysiłku redakcyjnego przy tej samej jakości.

Prośby o zastępstwa. Nauczyciel przesyła datę, lekcję, klasę i uwagi. Skrypt tworzy kartę zastępstwa, wysyła maila do administracji i dodaje nieobecność do wspólnego kalendarza kadry.

Wzorzec jest zawsze taki sam: formularz zbiera ustrukturyzowane dane wejściowe, Apps Script kieruje nimi i przekształca, a rezultat ląduje dokładnie tam, gdzie powinien. Nauczyciel robi jedną rzecz. Instytucja pamięta wszystko.


Słowo o AI i danych

Prompt wysyłany do Gemini zawiera tytuł i słowa kluczowe — nigdy nazwisk uczniów, numerów identyfikacyjnych ani danych osobowych. I tak ma zostać. AI pomaga w pisaniu; nie przetwarza danych osobowych. Jeśli nauczyciele trzymają się prostej zasady — opisz, co się wydarzyło, a nie kto był w to zaangażowany — system jest zgodny z RODO już na poziomie założeń.