0887 371 498 support@itservice-bg.net
Как да преместиш WordPress сайт без достъп до хостинг, база данни или пароли
01.08.2026 · Самуил Арсов · Hosting

Как да преместиш WordPress сайт без достъп до хостинг, база данни или пароли

Представете си следната ситуация, която е по-честа, отколкото ви се струва: имате си сайт на WordPress — новини, статии, стотици снимки, трупани с години — но сайтът се поддържа от някой друг. Външна фирма, бивш колега или човек, с когото пътищата ви са се разделили. Съдържанието е вашето, но достъпът е у него: няма cPanel, няма FTP, няма достъп до базата данни, няма и парола за wp-admin. И този някой, по една или друга причина, не иска да ви даде нищо — нито архив, нито експорт.

Класическият отговор е „значи си вързан“. Но не сте. Наскоро ми се наложи да преместя точно такъв сайт — от site.com на нов адрес site.bgбез нито един достъп до стария хостинг. Няколко хиляди публикации и десетки хиляди изображения бяха пренесени 1:1, със запазени адреси и дати. В тази статия ще обясня как става това, защо изобщо е възможно и къде са подводните камъни.

Накратко: всеки стандартен WordPress по подразбиране има публична врата към цялото си съдържание — REST API-то на адрес /wp-json/. През нея всеки може да прочете статиите, страниците, категориите и списъка с файлове — без парола. Оттам нататък е въпрос на организация да ги пресъздадеш на новия сайт.

Ключът: REST API-то на WordPress

От версия 4.7 (края на 2016 г.) насам всеки WordPress идва с вградено REST API — програмен интерфейс, през който темата, мобилните приложения и редакторът Gutenberg си говорят със сайта. Важното за нас е, че по подразбиране четенето на публичното съдържание е отворено за всички. Не защото някой е сбъркал настройка — така е замислено. Публикуваните статии така или иначе са видими за всеки посетител; API-то просто ги дава в подреден, машинно четим вид.

Пробата отнема пет секунди. Отваряте в браузъра (или с curl) адреса на сайта плюс /wp-json/wp/v2/posts:

curl -s "https://site.com/wp-json/wp/v2/posts?per_page=1" | jq '.[0] | {id, slug, date, title: .title.rendered}'

{
  "id": 4821,
  "slug": "otvoreno-zasedanie-na-obshtinskiya-savet",
  "date": "2026-05-14T09:32:11",
  "title": "Открито заседание на общинския съвет"
}

Ако получите такъв JSON — сайтът е ваш за сваляне (в смисъл на съдържание). API-то отдава няколко „колекции“, всяка на свой адрес:

Адрес (endpoint) Какво връща
/wp-json/wp/v2/posts Всички публикации — заглавие, съдържание, дата, slug, категории, featured изображение
/wp-json/wp/v2/pages Статичните страници (За нас, Контакти…)
/wp-json/wp/v2/categories Дървото от категории (с родител/дете)
/wp-json/wp/v2/tags Етикетите
/wp-json/wp/v2/media Медийната библиотека — с директни URL-и към всеки файл

С други думи, WordPress сам ви подава каталог на всичко, което съдържа. Задачата ни се свежда до три стъпки: 1) извличане на този каталог, 2) пресъздаване на съдържанието на новия сайт и 3) сваляне на самите файлове.

Стъпка 1: Извличане (сваляме каталога)

API-то не връща всичко наведнъж — дава максимум 100 записа на страница (параметър per_page=100) и се разлиства с page=1, 2, 3…. Колко страници има общо, ни го казва самият сървър в HTTP хедъра X-WP-TotalPages:

curl -sI "https://site.com/wp-json/wp/v2/posts?per_page=100" | grep -i x-wp-

x-wp-total: 3447
x-wp-totalpages: 35

3447 публикации, разпределени в 35 страници. Оттам нататък един прост цикъл сваля всичко в JSON файлове. Важен детайл: подреждаме по orderby=id&order=asc, за да е предвидим редът, а когато страниците свършат, WordPress връща грешка rest_post_invalid_page_number — това ни е сигналът за спиране:

DOMAIN="https://site.com"
mkdir -p data

for type in posts pages categories tags media; do
  page=1
  while : ; do
    out="data/${type}_$(printf '%03d' $page).json"
    curl -s "$DOMAIN/wp-json/wp/v2/$type?per_page=100&page=$page&orderby=id&order=asc" -o "$out"
    # свършиха ли страниците?
    if grep -q 'rest_post_invalid_page_number' "$out"; then rm "$out"; break; fi
    if [ "$(jq 'length' "$out")" -lt 1 ]; then rm "$out"; break; fi
    echo "  $type страница $page — ок"
    page=$((page+1))
  done
done

За голяма медийна библиотека (в моя случай над 50 000 файла = над 500 страници) това върви няколко минути и е добре да се пусне „на заден план“ (напр. с systemd-run или tmux), за да не прекъсне, ако затворите терминала. Накрая в папка data/ имаме пълен, локален препис на целия сайт — само че като „суровина“ в JSON.

Стъпка 2: Пресъздаване на новия сайт

На новия site.bg (там вече имаме пълен достъп — той е нашият) вдигаме чист WordPress и започваме да сипваме съдържанието обратно. Тук работим с WP-CLI — командния инструмент на WordPress. Има два реда, за които трябва да сме внимателни:

  • Първо категориите, после статиите. Категориите имат родител-дете йерархия, затова се създават на няколко „минавания“ — първо родителите, после децата — и през цялото време пазим таблица „стар ID → нов ID“, за да вържем после статиите правилно.
  • Запазваме slug и дата. Ако новата статия получи същия slug и същата дата на публикуване, крайният URL съвпада със стария — а това е решаващо за SEO (повече за това по-долу).

За хиляди статии не пускаме WP-CLI по веднъж на статия (много бавно) — по-удобно е кратък PHP скрипт, изпълнен през wp eval-file, който минава през JSON-а и вика wp_insert_post() за всеки запис. Ето сърцевината (опростена):

foreach (glob('data/posts_*.json') as $file) {
    foreach (json_decode(file_get_contents($file), true) as $p) {

        // пренасочваме всички стари адреси към новия домейн
        $body = preg_replace(
            '#https?://(www\.)?site\.com#i',
            'https://site.bg',
            $p['content']['rendered']
        );

        wp_insert_post([
            'post_title'   => $p['title']['rendered'],
            'post_content' => $body,
            'post_name'    => $p['slug'],          // същият slug
            'post_date'    => $p['date'],          // същата дата
            'post_status'  => 'publish',
            'post_type'    => 'post',
            'post_category' => map_categories($p['categories']), // стар→нов ID
        ]);
    }
}

Забележете реда с preg_replace — минаваме през цялото съдържание и пренаписваме всяко срещане на стария домейн към новия. Иначе снимките и вътрешните връзки в статиите щяха да сочат обратно към стария сайт, който по някое време ще изчезне.

Стъпка 3: Файловете (медийната библиотека)

Дотук пренесохме текста. Снимките обаче са реални файлове, които все още седят на стария хостинг. Добрата новина: API-то ни е дало директен URL към всеки от тях в полето source_url. А файловете в wp-content/uploads/ са публични — тоест свалят се без никаква парола, просто с curl.

Вадим списъка с всички URL-и от свалените JSON-и и ги теглим паралелно (по 6 наведнъж), като запазваме същата структура от папки — така новите пътища ще съвпаднат едно към едно със старите:

# всички адреси на файлове, без дубликати
jq -r '.[].source_url' data/media_*.json | sort -u > media-urls.txt
wc -l media-urls.txt        # напр. 53331 файла

# теглим паралелно, пазейки пътя wp-content/uploads/ГГГГ/ММ/...
xargs -P6 -n1 -I{} sh -c '
  url="{}"
  path=$(echo "$url" | sed -E "s#https?://[^/]+/##")
  mkdir -p "$(dirname "$path")"
  curl -sfL "$url" -o "$path"
' < media-urls.txt

След това остава да „запознаем“ новия WordPress с тези файлове — да ги регистрира като прикачени файлове (attachments) в базата, за да ги вижда в библиотеката и в статиите. Това пак става с малък PHP скрипт, който за всеки файл създава запис през wp_insert_attachment(), като пази съответствието „стар ID → нов ID“. Накрая за всяка статия задаваме featured изображението (голямата снимка в началото) според старото поле featured_media.

Подводните камъни (защото винаги ги има)

На теория звучи гладко. На практика има три места, където се спъва всеки, който го прави за пръв път — и е добре да ги знаете предварително.

1. Адресите (permalinks) и SEO

Пресен WordPress по подразбиране прави „грозни“ адреси от вида ?p=123. Старият сайт почти сигурно е ползвал /%postname%/ (адрес = заглавието). Ако не настроите новия сайт по същия начин, всички стари линкове в Google ще дадат 404 и трафикът пропада. Отиваме в Настройки → Постоянни връзки и избираме същата структура. Тъй като запазихме slug-овете на стъпка 2, новите адреси излизат идентични на старите — старите резултати в Google продължават да работят.

Съвет: при някои хостинг конфигурации бутонът „Запази“ на постоянните връзки не успява да презапише файла .htaccess сам. Ако след смяната статиите пак дават 404, впишете стандартните WordPress правила в .htaccess ръчно — и адресите тръгват.

2. Видеата и „скритото“ съдържание

Тук е най-коварното. Някои теми пазят част от съдържанието не в текста на статията, а в скрити полета (custom fields / post meta). Класически пример: видео публикация, при която YouTube клипът е записан в отделно поле на темата, а не в тялото на статията. Резултатът? В REST API-то полето content.rendered е празно — и тези статии идват без видеото.

Решението е елегантно: за засегнатите статии не гледаме API-то, а сваляме готовата HTML страница (както я вижда посетителят) и с регулярен израз изваждаме адреса на вградения клип:

# сваляме реалната страница и вадим YouTube embed-а
html=$(curl -s "https://site.com/nazvanie-na-statiyata/")
echo "$html" | grep -oE 'youtube\.com/embed/[A-Za-z0-9_-]+'

После вграждаме клипа обратно в новата статия като нормален <iframe>. Същият подход върши работа за всичко, което темата рисува от скрити полета — стига да се вижда в готовата страница, може да се извади оттам. Поуката: API-то дава каквото знае; браузърът вижда крайния резултат.

3. Миниатюрите (thumbnails)

WordPress не пази само оригиналната снимка — за всяка прави няколко изрязани размера (за списъци, за featured, за галерии). Тези размери зависят от темата. Свалихме оригиналите, но новата тема иска свои размери, които още не съществуват — затова в началото списъците и заглавните снимки може да излизат празни. Лек — една команда, която пре-генерира всички размери за новата тема:

wp media regenerate --yes --only-missing

За десетки хиляди снимки това е дълга операция (при мен ~2 часа), но е „пусни и забрави“ — най-добре пак на заден план.

Ами ако REST API-то е изключено?

Честен въпрос — не винаги вратата е широко отворена. Някои плъгини за сигурност или ръчни настройки затварят публичното API. Тогава има резервни варианти, по ред на удобство:

  • RSS фийдът (/feed/) — рядко го изключват, а често отдава пълния текст на последните статии. Не е толкова богат като API-то, но е начало.
  • Sitemap-ът (/sitemap.xml, /sitemap_index.xml) — дава пълен списък на всички адреси на сайта, който после може да се обходи (crawl-не) страница по страница.
  • Директно обхождане на HTML-а — най-трудоемкото: сваляш страниците и вадиш съдържанието от тях. Работи винаги (щом сайтът е онлайн), но иска повече чистене.

В над 90% от случаите обаче API-то е отворено — просто защото никой не се е сетил да го затвори, а и повечето теми разчитат на него.

Малко здрав разум накрая

Всичко описано тук вади само публично видимо съдържание — това, което така или иначе всеки посетител може да прочете. Тук няма разбиване на пароли, няма влизане в чужда база данни, няма заобикаляне на защита. Просто помолваме сайта да ни даде в подреден вид това, което вече показва на всички. Естествено — правете го за своето собствено съдържание (или с изричното съгласие на собственика му). Целта на този пример е точно тази: да си вземете обратно това, което е ваше, когато някой го държи „заложник“.

Изводът

„Нямам достъп до хостинга, значи не мога да си взема сайта“ е мит. Докато сайтът е онлайн, той сам раздава съдържанието си през вратата, която WordPress отваря по подразбиране. С малко търпение и няколко реда скрипт се пренасят хиляди статии и десетки хиляди снимки — със запазени адреси, дати и SEO — на хостинг, който вие контролирате.

Поуката, ако трябва да я сведем до едно изречение: съдържанието ви никога не бива да е заложник на нечия парола. А ако някой в момента ви държи сайта и не иска да ви даде достъп — вече знаете, че това не е задънена улица.