Jak převést WordPress web na statické stránky pomocí Eleventy

Aktualizace (říjen 2026): Tenhle web už neběží na samostatném Eleventy projektu, který v článku popisuju. Přestěhoval jsem ho do svého systému Stránky na pohodu, ve kterém stavím a spravuju weby pro živnostníky (postavený je taky na Eleventy). Články, adresy i fotky zůstaly. Postup níže pořád platí, pokud si převod chcete udělat sami. A pokud ne, převedu váš web za vás.

Mnoho let jsem pro blogování používal WordPress. Přes jeho výhody mi vadily nároky na údržbu webu. Párkrát se mi dokonce stalo, že můj web někdo napadl přes bezpečnostní díru v některém z pluginů, a musel jsem to složitě řešit. Navíc web na WordPressu není nejrychlejší, pokud nevěnujete hodně péče optimalizaci. A protože jsem svůj web neaktualizoval moc často, rozhodl jsem se přejít na statický web, který vytvoří generátor statických stránek. Z velkého výběru u mě vyhrálo Eleventy.

Upozornění: Návod je vhodný pro technicky zdatnější uživatele.

Tip: Na konci článku najdete odkaz na repozitář na GitHubu s nakonfigurovaným Eleventy a šablonami, stejný, jaký jsem na tomto webu používal do roku 2026.

Rozdíly mezi WordPressem a Eleventy

Web postavený na WordPressu je dynamický. Jde o sadu PHP skriptů, které ve chvíli, kdy návštěvník otevře nějakou adresu, vytáhnou z databáze obsah, sestaví HTML stránku a pošlou ji zpět (zjednodušeně řečeno). Pokud web nepoužívá žádnou formu cache, stránka se skládá při každém požadavku, a to nějaký čas trvá.

Naproti tomu Eleventy (a ostatní generátory statických stránek) vygeneruje výsledné HTML stránky jednou, při buildu. Hotové stránky se uloží a pak už se jen posílají návštěvníkům. Hostovat statický web je jednoduché a často to nic nestojí. Je to velmi rychlé a ušetří to i práci s údržbou: pokud na webu nechcete nic měnit, nemusíte nic dělat.

U WordPressu obsah snadno vytváříte v administraci, kde ve WYSIWYG editoru píšete text, vkládáte fotky a případně nastavujete web a pluginy. Eleventy žádnou administraci nemá. Pracuje se soubory v adresáři projektu: články píšete v obyčejných textových souborech a formátujete je pomocí syntaxe markdown.

Mé požadavky na nový web

  1. Obsah uložený v běžném formátu markdown, aby byl použitelný sám o sobě a snadno přenesitelný, kdybych chtěl od Eleventy odejít.
  2. Jednoduché přidávání nového obsahu.
  3. Automatická optimalizace obrázků pro rychlejší načítání stránek.
  4. Možnost jednoduše vytvářet galerie fotek bez složitého nastavování: jen vytvořit adresář s fotkami a do textu vložit název galerie.

První požadavek se mi vyplatil. Když jsem web v roce 2026 stěhoval do Stránek na pohodu, články v markdownu šly přenést bez přepisování.

Plán migrace

Pokud se rozhodnete převést web, rozmyslete si, co všechno z něj chcete přenést. Pro mě byly hlavní články blogu. Čím víc speciálních funkcí jste ve WordPressu používali (různé pluginy, galerie, formátování), tím náročnější převod bude. Doporučuju si sepsat seznam, podle kterého pak pojedete, ať na nic nezapomenete.

Postup krok za krokem

Popíšu tady obecný postup, ten váš se může lišit podle toho, co od nového webu čekáte.

Převedení článků do formátu markdown

  1. Přihlaste se do administrace WordPressu. V levém menu vyberte Nástroje → Export, zvolte Veškerý obsah a stáhněte si data do počítače. Výsledkem bude jeden XML soubor.
  2. K převodu použijte nástroj wordpress-export-to-markdown. Potřebujete k němu Node.js, samotný nástroj instalovat nemusíte.
  3. Spusťte ho příkazem npx wordpress-export-to-markdown a průvodce vás provede možnostmi převodu. Když zvolíte i stahování obrázků, budou se postupně stahovat. Tip: několikrát mi stahování obrázků selhalo a ne všechny se stáhly, musel jsem nástroj pustit vícekrát.
  4. Hromadně (třeba v textovém editoru) nahraďte absolutní adresy článků a obrázků v rámci webu relativními, tedy místo https://nogol.cz/tag/projekty/ jen /tag/projekty/. Díky relativním cestám budou odkazy fungovat i při testování na vlastním počítači.
  5. Ručně projděte jednotlivé markdown soubory a dolaďte formátování a případné problémy.

Extra tip: pokud chcete převést i komentáře, musíte je z exportovaného XML souboru vytáhnout a přidat do existujících souborů. Takový skript jsem si nechal napsat ChatGPT, stačilo pár iterací a dělal, co potřebuju: vytáhne komentáře, seřadí je podle času a vloží je do správných článků. Ke spuštění potřebujete nainstalovat xml2js příkazem npm install xml2js. Ve skriptu pak stačí upravit cestu k exportu, případně doladit strukturu, v jaké máte články uložené.

Skript pouštějte jen jednou: komentáře připisuje na konec souborů, takže druhé spuštění by je přidalo znovu. Přenáší jen schválené komentáře a vynechává zpětné odkazy z jiných webů (pingbacky a trackbacky). Text komentářů vkládá tak, jak je uložený ve WordPressu, tedy včetně případného HTML.

const fs = require('fs');
const path = require('path');
const xml2js = require('xml2js');

// Cesta k exportu z WordPressu
const WORDPRESS_EXPORT_PATH = path.join(__dirname, 'WordPress.xml');

// Cesta k adresáři s Markdown soubory příspěvků
const POSTS_DIR = path.join(__dirname, 'post');

// Funkce pro převedení data na čitelný formát
function formatDate(dateStr) {
    const date = new Date(dateStr);
    return date.toLocaleString('cs-CZ', { timeZone: 'Europe/Prague' });
}

let commentCount = 0;

// Načtení a parsování XML souboru
console.log('Čtu XML soubor...');
fs.readFile(WORDPRESS_EXPORT_PATH, (err, data) => {
    if (err) {
        console.error('Chyba při čtení souboru:', err);
        return;
    }

    console.log('Parsování XML...');
    xml2js.parseString(data, { trim: true }, (err, result) => {
        if (err) {
            console.error('Chyba při parsování XML:', err);
            return;
        }

        const items = result.rss.channel[0].item;
        if (!items || items.length === 0) {
            console.log('Žádné příspěvky nebyly nalezeny v exportu.');
            return;
        }

        console.log(`Nalezeno ${items.length} příspěvků.`);

        items.forEach((item, index) => {
            const title = item.title ? item.title[0] : 'Bez názvu';
            // Jen schválené komentáře, bez pingbacků a trackbacků
            const comments = (item['wp:comment'] || []).filter((comment) => {
                const approved = comment['wp:comment_approved'] ? comment['wp:comment_approved'][0] : '0';
                const type = comment['wp:comment_type'] ? comment['wp:comment_type'][0] : '';
                return approved === '1' && type !== 'pingback' && type !== 'trackback';
            });

            // Získání post_name a post_date pro určení cesty k index.md
            const postName = item['wp:post_name'] ? item['wp:post_name'][0] : null;
            const postDateStr = item['wp:post_date'] ? item['wp:post_date'][0] : null;

            if (!postName || !postDateStr) {
                console.warn(`Příspěvek "${title}" nemá vyplněný wp:post_name nebo wp:post_date. Přeskakuji.`);
                return;
            }

            // Parsování data pro získání roku a měsíce
            const postDate = new Date(postDateStr);
            if (isNaN(postDate)) {
                console.warn(`Příspěvek "${title}" má neplatné datum: ${postDateStr}. Přeskakuji.`);
                return;
            }
            const year = postDate.getFullYear().toString();
            const month = String(postDate.getMonth() + 1).padStart(2, '0'); // Měsíc od 1 do 12

            // Konstrukce cesty k index.md
            const indexPath = path.join(POSTS_DIR, year, month, postName, 'index.md');

            // Kontrola existence index.md
            if (!fs.existsSync(indexPath)) {
                console.warn(`Soubor ${indexPath} neexistuje pro příspěvek "${title}". Přeskakuji.`);
                return;
            }

            console.log(`Příspěvek ${index + 1}: "${title}" - nalezeno ${comments.length} komentářů.`);

            if (comments.length > 0) {
                // Seřazení komentářů podle data (od nejstaršího po nejnovější)
                comments.sort((a, b) => {
                    const dateA = new Date(a['wp:comment_date'][0]);
                    const dateB = new Date(b['wp:comment_date'][0]);
                    return dateA - dateB;
                });

                // Vytvoření sekce komentářů
                let commentsMarkdown = '\n\n## Komentáře\n\n';

                comments.forEach(comment => {
                    const commentAuthor = comment['wp:comment_author'] ? comment['wp:comment_author'][0] : 'Neznámý autor';
                    const commentAuthorURL = comment['wp:comment_author_url'] ? comment['wp:comment_author_url'][0] : null;
                    const commentDateStr = comment['wp:comment_date'] ? comment['wp:comment_date'][0] : '';
                    const commentDate = commentDateStr ? formatDate(commentDateStr) : 'Neznámé datum';
                    const commentContent = comment['wp:comment_content'] ? comment['wp:comment_content'][0] : '';

                    // Filtrování prázdných komentářů
                    if (!commentContent) {
                        return;
                    }

                    // Formátování autora s odkazem, pokud URL existuje
                    let formattedAuthor;
                    if (commentAuthorURL && commentAuthorURL.trim() !== '') {
                        formattedAuthor = `[${commentAuthor}](${commentAuthorURL})`;
                    } else {
                        formattedAuthor = commentAuthor;
                    }

                    commentsMarkdown += `### ${formattedAuthor}\nDatum: ${commentDate}\n${commentContent.replace(/\r?\n/g, '  \n')}\n\n`;
                });

                // Zapsání komentářů do index.md
                fs.appendFile(indexPath, commentsMarkdown, (err) => {
                    if (err) {
                        console.error(`Chyba při zápisu do souboru ${indexPath}:`, err);
                        return;
                    }
                    console.log(`Komentáře byly úspěšně přidány do souboru ${indexPath}`);
                });

                commentCount += comments.length;
            }
        });

        if (commentCount === 0) {
            console.log('Žádné komentáře nebyly nalezeny.');
        }
    });
});

Pokud chcete zachovat stejné adresy jako na původním webu, můžete:

  1. Uložit soubory do adresářové struktury, která odpovídá adrese. Například článek https://nogol.cz/2025/01/nova-verze-blogu-a-odchod-od-wordpressu/ bude v adresáři src/2025/01/nova-verze-blogu-a-odchod-od-wordpressu/ jako soubor index.md.
  2. Nebo adresu nastavit ve frontmatteru (vysvětlím níže) proměnnou permalink.

Nakonfigurujte si .eleventy.js

Tento soubor (v novějších verzích Eleventy se může jmenovat i eleventy.config.js) určuje, odkud a kam se web generuje, jak se mají zpracovat obrázky a jaké pluginy se použijí. Vycházejte z příkladů v dokumentaci Eleventy nebo z mého repozitáře uvedeného na konci článku.

Vytvořte si šablony

Každý .md soubor má na začátku (mezi dvěma řádky ---) metadata, takzvaný frontmatter, která se použijí při generování HTML stránky. Eleventy ho umí ve formátu YAML, JSON i JavaScript. Můžete v něm definovat různé proměnné. Jednou z nich je layout, tedy název šablony, do které se obsah stránky vloží.

Například začátek souboru s tímto článkem vypadal na původním webu takto:

---
title: "Jak převést WordPress web na statické stránky pomocí Eleventy"
date: "2025-01-16"
type: "blogpost"
tags:
  - "Programování"
layout: post-detail.njk
---

Abyste nemuseli layout psát do každého článku, můžete ho nastavit pro celou složku souborem s daty adresáře. V Eleventy se jmenuje podle složky, třeba blog/blog.11tydata.json.

Šablony se dají skládat do sebe. Například jedna šablona pro celou stránku, další z ní vychází a zobrazuje detail článku, jiná výpis článků a tak dál. Pro pochopení se podívejte do kódu v repozitáři na konci článku, je v něm vidět, jak byl tento web poskládaný.

Eleventy nabízí mnoho jazyků, ve kterých můžete šablony psát. Já zvolil Nunjucks.

Web si otestujte lokálně

Příkazem npx @11ty/eleventy --serve spustíte build webu: statické stránky se vygenerují do vámi nastaveného adresáře a spustí se lokální server, kde si výsledek prohlédnete v prohlížeči na adrese http://localhost:8080/. Když upravíte některý ze souborů, web se znovu sestaví a v prohlížeči hned vidíte změnu. Pokud se změny stylů neprojeví (typicky když CSS zpracováváte mimo Eleventy), přidejte jejich složku mezi sledované pomocí eleventyConfig.addWatchTarget().

Publikace webu

Ruční varianta

Nejjednodušší možnost je příkazem npx @11ty/eleventy vygenerovat celý web do jednoho adresáře a ten nahrát přes FTP k vašemu hostingu. A máte hotovo. Pokud ale na webu budete dělat změny, celý tenhle postup vás čeká při každé z nich. Proto jsem tuhle možnost zavrhl, nechtělo by se mi to při každé úpravě řešit.

Automatizovaná varianta

Hledal jsem pohodlnější způsob, jak web aktualizovat. Celý projekt mám uložený v repozitáři na GitHubu. K hostování jsem zvolil Netlify, které má pro malé projekty bezplatný tarif (podmínky a limity se od té doby změnily, aktuální najdete v ceníku Netlify).

Při přidávání webu na Netlify zvolíte Import from existing project → GitHub, vyberete repozitář s projektem, udělíte Netlify oprávnění a nastavíte, kterou větev má Netlify hlídat.

Když pak chcete přidat nový článek nebo upravit stávající, stačí změny pushnout do zvolené větve. Netlify je pozná, spustí build a do pár minut máte web aktuální.

BONUS: Ukázkový repozitář

Pokud si to chcete vyzkoušet, připravil jsem ukázkový repozitář, na kterém si můžete vyzkoušet, jak se s takovým webem pracuje. Stačí ho naklonovat, spustit npm install a npm start a prohlédnout si výsledek. Rovnou si můžete s webem pohrát a přidávat nové stránky. A třeba si ho přizpůsobit a postavit na něm vlastní web :)

Repozitář najdete zde: https://github.com/davidnogol/eleventy_blog_example

Napište mi