Przejdź do głównej treści

Generator PRD

Obraz PRD

Generator PRD to lekka aplikacja webowa typu client-side, umożliwiająca szybkie tworzenie dokumentów Product Requirements Document (PRD) w formacie Markdown.

Aplikacja:

  • działa w przeglądarce (bez backendu),
  • wykorzystuje HTML + Bootstrap 5.3 + Vanilla JavaScript,
  • generuje spójny, gotowy do użycia dokument PRD,
  • pozwala na dynamiczne rozszerzanie struktury dokumentu o własne sekcje,
  • umożliwia szybkie wypełnienie formularza danymi przykładowymi.

Docelowe zastosowania:

  • dokumentacja produktowa,
  • repozytoria Git (README.md),
  • Confluence / Notion / Obsidian,
  • praca PM / PO / Tech Leada.

Architektura aplikacji

Aplikacja składa się z trzech głównych warstw:

Warstwa UI (Bootstrap 5.3)

  • Responsywny layout oparty o container, row, col-12
  • Formularz wejściowy po lewej stronie
  • Podgląd wygenerowanego Markdown po prawej stronie
  • Karty (card) dla czytelności
  • Brak zewnętrznych zależności JS

Logika JavaScript

  • Zbieranie danych z formularza
  • Normalizacja list (automatyczne - w Markdown)
  • Dynamiczne sekcje (dodawanie / usuwanie)
  • Generator tekstu Markdown
  • Kopiowanie do schowka

Output

  • Czysty Markdown
  • Brak HTML / formatowania wizualnego
  • Gotowy do wklejenia lub zapisania jako .md

Struktura generowanego PRD

Aplikacja generuje dokument w następującej strukturze:

# PRD – [Nazwa produktu]

## 1. Cel produktu

## 2. Problem użytkownika

## 3. Zakres
### In scope
### Out of scope

## 4. Wymagania
### Funkcjonalne
### Niefunkcjonalne

## 5. Metryki sukcesu

## [Sekcje dynamiczne...]

---
_Dokument wygenerowany automatycznie_

Każda sekcja:

  • może być pusta (wtedy wstawiane jest Brak)
  • listy są automatycznie formatowane jako Markdown

Instrukcja użycia (krok po kroku)

Krok 1: Uruchomienie aplikacji

  1. Otwórz plik .html w przeglądarce lub
  2. Wrzuć go na dowolny hosting statyczny (np. GitHub Pages)

Nie jest wymagany serwer ani build.

Krok 2: Szybki start (szablon)

Kliknij przycisk:
„Wypełnij przykładem”

Aplikacja:

  • uzupełni wszystkie pola realistycznymi danymi,
  • doda przykładowe sekcje dynamiczne (np. User Stories, Ryzyka),
  • pozwoli Ci od razu wygenerować PRD i zobaczyć strukturę.

To najlepszy sposób, żeby zrozumieć, jak działa generator.

Krok 3: Wypełnianie formularza

Informacje podstawowe

  • Nazwa produktu – tytuł PRD
  • Cel produktu – dlaczego produkt powstaje
  • Problem użytkownika – jaki problem rozwiązuje

Zakres

  • In scope – elementy objęte projektem
  • Out of scope – elementy wyraźnie wyłączone

Każda linia = jeden punkt listy Nie musisz wpisywać - (aplikacja zrobi to sama).

Wymagania

  • Funkcjonalne – co system musi robić
  • Niefunkcjonalne – jakość, wydajność, SLA, bezpieczeństwo

Metryki sukcesu

  • KPI
  • cele biznesowe
  • mierzalne efekty wdrożenia

Krok 4: Sekcje dynamiczne

Sekcje dynamiczne służą do rozszerzania PRD o dowolne bloki, np.:

  • User Stories
  • Edge cases
  • Ryzyka
  • Założenia
  • Ograniczenia techniczne
  • Open questions

Dodawanie sekcji

Kliknij:
„+ Dodaj sekcję”

W każdej sekcji:

  • Tytuł → staje się nagłówkiem ##
  • Treść → renderowana jako lista Markdown

Sekcję można usunąć przyciskiem „Usuń”.

Krok 5: Generowanie Markdown

Kliknij:
„Generuj Markdown”

Markdown pojawi się:

  • po prawej stronie,
  • w całości edytowalny do skopiowania,
  • zawsze w aktualnym stanie formularza.

Krok 6: Pobieranie

Kliknij:
„Pobierz md”

Markdown zostanie pobrany i zapisany na dysku.

Ograniczenia

  • brak zapisu danych (brak localStorage)
  • brak walidacji biznesowej treści
  • brak wersjonowania dokumentu

Typowe use-case’y

  • szybkie PRD dla MVP
  • dokumentacja funkcji do sprintu
  • pre-work przed refinementem
  • specyfikacja do handoveru devom
  • spójne PRD w repozytoriach

Informacje podstawowe

Zakres

Wymagania

Metryki

Sekcje dynamiczne

Wynik

Kliknij "Generuj Markdown" aby zobaczyć wynik...

Kod po stronie przeglądarki

<link type="text/css" href="http://www.dariuszrorat.ugu.pl/assets/css/bootstrap/wcag-outline.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://unpkg.com/docx@8.5.0/build/index.umd.js"></script>
<div id="app">
    <div class="row g-4">        
        <div class="col-12">
            <form id="prdForm" class="card card-body shadow-sm">
                <h3 class="h5 mb-3">Informacje podstawowe</h3>
                <div class="mb-3">
                    <label for="productName" class="form-label">Nazwa produktu</label>
                    <input type="text" class="form-control" id="productName" required>
                </div>
                <div class="mb-3">
                    <label for="productGoal" class="form-label">Cel produktu</label>
                    <textarea class="form-control" id="productGoal" rows="10"></textarea>
                </div>
                <div class="mb-3">
                    <label for="userProblem" class="form-label">Problem użytkownika</label>
                    <textarea class="form-control" id="userProblem" rows="10"></textarea>
                </div>
                <h3 class="h5 mt-4 mb-3">Zakres</h3>
                <div class="mb-3">
                    <label for="inScope" class="form-label">Zakres (In scope)</label>
                    <textarea class="form-control" id="inScope" rows="10" placeholder="- Funkcja A\n- Funkcja B"></textarea>
                </div>
                <div class="mb-3">
                    <label for="outScope" class="form-label">Poza zakresem (Out of scope)</label>
                    <textarea class="form-control" id="outScope" rows="10"></textarea>
                </div>
                <h3 class="h5 mt-4 mb-3">Wymagania</h3>
                <div class="mb-3">
                    <label for="functionalReq" class="form-label">Wymagania funkcjonalne</label>
                    <textarea class="form-control" id="functionalReq" rows="10"></textarea>
                </div>
                <div class="mb-3">
                    <label for="nonFunctionalReq" class="form-label">Wymagania niefunkcjonalne</label>
                    <textarea class="form-control" id="nonFunctionalReq" rows="10"></textarea>
                </div>
                <h3 class="h5 mt-4 mb-3">Metryki</h3>
                <div class="mb-3">
                    <label for="successMetrics" class="form-label">Kryteria sukcesu</label>
                    <textarea class="form-control" id="successMetrics" rows="10"></textarea>
                </div>
                <h3 class="h5 mt-4 mb-3">Sekcje dynamiczne</h3>
                <div id="dynamicSections"></div>
                <button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addSection()">+ Dodaj sekcję</button>
            </form>
            <div class="d-flex gap-2 mt-2">
                <button type="button" class="btn btn-primary" onclick="generateMarkdown()">Generuj Markdown</button>
                <button type="button" class="btn btn-outline-secondary" onclick="loadExample()">Wypełnij przykładem</button>
            </div>
        </div>        
        <div class="col-12">
            <div class="card card-body shadow-sm">
                <div class="d-flex justify-content-between align-items-center mb-2">
                    <h3 class="h5 mb-0">Wynik</h3>
                    <div class="btn-group">
                        <button id="downloadMdBtn" class="btn btn-sm btn-outline-secondary"><i class="bi bi-filetype-md"></i> Pobierz md</button>
                        <button id="downloadDocxBtn" class="btn btn-sm btn-outline-secondary"><i class="bi bi-filetype-docx"></i> Pobierz docx</button>                        
                    </div>
                </div>
                <ul class="nav nav-underline mt-3">
                    <li class="nav-item" role="presentation">
                        <button class="nav-link text-body active" id="tab-mdview-tab" data-bs-toggle="tab" data-bs-target="#tab-mdview" type="button" role="tab">Markdown</button>  
                    </li>
                    <li class="nav-item" role="presentation">
                        <button class="nav-link text-body" id="tab-htmlview-tab" data-bs-toggle="tab" data-bs-target="#tab-htmlview" type="button" role="tab">HTML</button>            
                    </li>
                </ul>
                <div class="tab-content">
                    <div id="tab-mdview" class="tab-pane fade show active" role="tabpanel">
                        <pre><code id="md-preview" class="language-markdown">Kliknij "Generuj Markdown" aby zobaczyć wynik...</code></pre>
                    </div>
                    <div id="tab-htmlview" class="tab-pane fade" role="tabpanel">
                        <div id="html-preview" class="p-3"></div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
<script>
const { Document, Packer, Paragraph, HeadingLevel, TextRun } = window.docx;       
const downloadMdBtn = document.getElementById('downloadMdBtn');
const downloadDocxBtn = document.getElementById('downloadDocxBtn');
const mdPreview = document.getElementById('md-preview');
const htmlPreview = document.getElementById('html-preview');

let sectionCount = 0;
let generated = false;

function mdList(text) {
    if (!text.trim()) return "- Brak";
    return text;
}

function escapeHtml(s) {
    if (!s) return '';
    return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}

function generateMarkdown() {
    const productName = document.getElementById('productName').value;
    const productGoal = document.getElementById('productGoal').value;
    const userProblem = document.getElementById('userProblem').value;
    const inScope = document.getElementById('inScope').value;
    const outScope = document.getElementById('outScope').value;
    const functionalReq = document.getElementById('functionalReq').value;
    const nonFunctionalReq = document.getElementById('nonFunctionalReq').value;
    const successMetrics = document.getElementById('successMetrics').value;

    let dynamicMd = '';
    document.querySelectorAll('.dynamic-section').forEach(sec => {
        const title = sec.querySelector('.section-title').value;
        const body = sec.querySelector('.section-body').value;
        if (title.trim()) {
            dynamicMd += `\n## ${title}\n${mdList(body)}\n`;
        }
    });

    const md = `# PRD – ${productName}

## 1. Cel produktu
${productGoal || 'Brak'}

## 2. Problem użytkownika
${userProblem || 'Brak'}

## 3. Zakres

### In scope
${mdList(inScope)}

### Out of scope
${mdList(outScope)}

## 4. Wymagania

### Funkcjonalne
${mdList(functionalReq)}

### Niefunkcjonalne
${mdList(nonFunctionalReq)}

## 5. Metryki sukcesu
${mdList(successMetrics)}

${dynamicMd}
---
_Dokument wygenerowany automatycznie_`;

    //editor.setValue(md);
    mdPreview.textContent = md;
    htmlPreview.innerHTML = marked.parse(md);

    const el = mdPreview;
    if (el) {
        if (el.hasAttribute('data-highlighted'))
            el.removeAttribute('data-highlighted');
        if (el.hasAttribute('data-highlighter'))
            el.removeAttribute('data-highlighter');
        if (el.classList.contains('hljs'))
            el.classList.remove(...Array.from(el.classList).filter(cls => cls.startsWith('hljs')));
        el.innerHTML = escapeHtml(el.textContent);
        hljs.highlightElement(el);
    }

    generated = true;

    return md;
}
    
function markdownToDocx(md) {
    const tokens = marked.lexer(md);
    const elements = [];

    tokens.forEach(token => {

        if (token.type === "heading") {
            elements.push(
                new Paragraph({
                    text: token.text,
                    heading: token.depth === 1
                        ? HeadingLevel.HEADING_1
                        : HeadingLevel.HEADING_2
                })
            );
        }

        else if (token.type === "paragraph") {
            elements.push(
                new Paragraph(token.text)
            );
        }

        else if (token.type === "list") {
            token.items.forEach(item => {
                elements.push(
                    new Paragraph({
                        text: item.text,
                        bullet: { level: 0 }
                    })
                );
            });
        }

        else if (token.type === "hr") {
            elements.push(
                new Paragraph({
                    children: [new TextRun("------------------------")]
                })
            );
        }
    });

    return elements;
}
    
downloadMdBtn.addEventListener('click', () => {
    const md = generated ? mdPreview.textContent : generateMarkdown();
    const productName = document.getElementById('productName').value;
    const blob = new Blob([md], {
        type: 'text/markdown;charset=utf-8'
    });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    const safeName = (productName || 'prd').trim().replace(/[^a-z0-9-_]/gi, '_').toLowerCase();
    a.download = `${safeName || 'prd'}.md`;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
});

downloadDocxBtn.addEventListener('click', () => {
    const markdown = generated ? mdPreview.textContent : generateMarkdown();
    const productName = document.getElementById('productName').value;    
    const safeName = (productName || 'prd').trim().replace(/[^a-z0-9-_]/gi, '_').toLowerCase();
    
    const doc = new Document({
        sections: [{
            children: markdownToDocx(markdown)
        }]
    });

    Packer.toBlob(doc).then(blob => {
        const link = document.createElement("a");
        link.href = URL.createObjectURL(blob);
        link.download = `${safeName || 'prd'}.docx`;
        link.click();
    });
    
});
    
function addSection(title = '', body = '') {
    sectionCount++;
    const div = document.createElement('div');
    div.className = 'dynamic-section border rounded p-2 mb-2';
    div.innerHTML = `
    <input class="form-control mb-2 section-title" placeholder="Tytuł sekcji" value="${title}" aria-label="Tytuł sekcji">
    <textarea class="form-control mb-2 section-body" rows="10" placeholder="- Punkt 1\n- Punkt 2" aria-label="Treść sekcji">${body}</textarea>
    <button class="btn btn-sm btn-outline-danger" type="button" onclick="this.parentElement.remove()">Usuń</button>
  `;
    document.getElementById('dynamicSections').appendChild(div);
}

function loadExample() {
    document.getElementById('productName').value = 'Aplikacja do zarządzania zadaniami';
    document.getElementById('productGoal').value = 'Uproszczenie zarządzania zadaniami zespołu produktowego.';
    document.getElementById('userProblem').value = 'Użytkownicy gubią zadania między różnymi narzędziami.';
    document.getElementById('inScope').value = '- Tworzenie zadań\n- Statusy\n- Powiadomienia';
    document.getElementById('outScope').value = '- Faktury\n- Integracje księgowe';
    document.getElementById('functionalReq').value = '- CRUD zadań\n- Przypisywanie użytkowników';
    document.getElementById('nonFunctionalReq').value = '- Dostępność 99.9%\n- Czas odpowiedzi < 300ms';
    document.getElementById('successMetrics').value = '- 30% wzrost aktywnych użytkowników';

    document.getElementById('dynamicSections').innerHTML = '';
    addSection('User Stories', '- Jako użytkownik chcę dodawać zadania\n- Jako manager chcę widzieć postęp');
    addSection('Ryzyka', '- Niska adopcja\n- Brak integracji');
}
</script>

Kod po stronie serwera

Brak kodu serwera

Ta aplikacja działa wyłącznie w przeglądarce i nie korzysta z kodu po stronie serwera.

Licencja

## BSD-3-Clause License Agreement
BSD-3-Clause
Сopyright (c) 2026 Dariusz Rorat

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 stycznia 2026 3

Kategorie

Technologie

Dziękujemy!
()

Informacja o cookies

Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.