# WordPress

Ai deja un site WordPress? Îndreaptă-ți blogul spre el și fiecare articol pe
care îl publici aici e trimis acolo automat — ca un articol WordPress real,
în tema ta, la permalinkurile tale. **Fără plugin de instalat.** WordPress
core are tot ce e nevoie încă din versiunea 5.6: API-ul REST și parolele de
aplicație.

:::callout{severity=info title="Nu trebuie să alegi pentru totdeauna"}
Modul de livrare e per blog și îl poți schimba oricând înapoi. Nimic din
conținut nu se schimbă — același articol revizuit, cu backlinkuri, bogat în
MD2, fie e servit pe domeniul tău de noi, fie e scris în WordPress-ul tău.
:::

## Conectare în trei pași

::::steps

:::step[Creează o parolă de aplicație în WordPress]
În WordPress, mergi la **Utilizatori → Profil**, derulează la **Parole de
aplicație**, scrie un nume (`baas`) și apasă **Adaugă o parolă de aplicație
nouă**. WordPress arată o parolă de 24 de caractere **o singură dată** —
copiaz-o acum.

Conectează-te ca **administrator** sau **editor**. Aceste roluri dețin
capacitatea `unfiltered_html`, care e ceea ce permite conținutului tău bogat
să ajungă intact — vezi **Ce supraviețuiește trimiterii** mai jos.
:::

:::step[Lipește-o în cardul de livrare al blogului]
Pe pagina blogului tău din baas, găsește **Unde ajung articolele**, comută
livrarea pe **Trimite în WordPress** și completează:

| Câmp | Valoare |
|---|---|
| URL-ul site-ului WordPress | Rădăcina site-ului tău, ex. `https://example.com` — nu URL-ul `/wp-admin` |
| Utilizator WordPress | **Numele de autentificare** al acelui utilizator, nu emailul lui |
| Parolă de aplicație | Șirul de 24 de caractere, cu tot cu spații |
| Articolele ajung ca | `Publicate`, sau `Ciornă` dacă vrei o privire finală în interiorul WordPress |

Stocăm parola criptată (AES-256-GCM) și n-o mai afișăm niciodată — câmpul
rămâne gol la fiecare vizită ulterioară, iar salvarea cu el gol păstrează
parola stocată.
:::

:::step[Testează conexiunea]
Apasă **Testează conexiunea**. Un rezultat verde numește utilizatorul
WordPress cu care ne-am autentificat. Orice altceva îți spune exact ce a zis
WordPress — vezi **Depanare** mai jos.
:::

::::

## Ce se trimite, și când

Un articol e trimis când **devine publicat** — în momentul în care îl aprobi
în coada de revizuire, sau în momentul în care auto-publicarea îl publică
pentru tine. Trimiterea poartă:

| Câmp WordPress | De unde |
|---|---|
| `title` | Titlul articolului |
| `slug` | Slug-ul articolului — permalinkul tău rămâne predictibil |
| `content` | HTML-ul randat, împachetat și stilizat (mai jos) |
| `excerpt` | Rezumatul generat |
| `status` | Orice ai ales la **Articolele ajung ca** |
| `date_gmt` | Ora de publicare înregistrată aici |

Trimiterea din nou **actualizează același articol WordPress** în loc să
creeze unul al doilea: reținem id-ul articolului WordPress. Dacă ștergi
articolul din interiorul WordPress, următoarea trimitere îl recreează.

:::callout{severity=success title="O trimitere eșuată nu depublică niciodată nimic"}
Dacă WordPress e nefuncțional, la mijlocul unei actualizări sau în spatele
unui firewall, trimiterea reîncearcă cu backoff, iar articolul rămâne exact
așa cum e publicat aici. Vezi un badge roșu **Eșuat** cu eroarea reală, și un
buton **Trimite din nou** odată ce ai reparat problema. Problemele de
livrare nu au voie niciodată să devină probleme de conținut.
:::

### Retragerea unui articol

**Retrage articolul** îl scoate și de aici, și din WordPress: articolul nu
mai este servit de noi, iar copia din WordPress revine la **ciornă**, deci
răspunde 404 pe site-ul tău, în timp ce conținutul rămâne în CMS-ul tău. Nu
ștergem niciodată un articol de pe site-ul tău și nu îl trimitem la coșul de
gunoi — ciorna păstrează același id de articol WordPress, așa că **Publică
din nou** scrie peste același articol în loc să creeze unul al doilea.

Dacă WordPress nu poate fi contactat, retragerea reîncearcă cu backoff și
cardul arată eroarea; articolul e deja retras aici, oricum.

## Ce supraviețuiește trimiterii

Articolele noastre sunt mai bogate decât HTML simplu — callout-uri, ghiduri
pas cu pas, panouri de comparație, grafice, cod adnotat. WordPress decide
cât de mult din asta acceptă în funcție de o singură capacitate a
utilizatorului cu care te-ai conectat: `unfiltered_html`.

::::compare{title="Cu ce utilizator te-ai conectat?"}

:::option[Administrator sau editor]{severity=success}
Deține `unfiltered_html` pe o instalare single-site.

- Stilizare completă a componentelor — trimitem un bloc `<style>` scoped
- Grafice, diagrame de flux, sparkline-uri (SVG) — toate se randează
- [Paleta de temă a blogului tău](/ro/docs/theming) călătorește odată cu conținutul
- Stilurile sunt scoped la wrapper-ul nostru și nu îți pot repicta tema
:::

:::option[Autor, colaborator, sau orice utilizator multisite]{severity=warning}
Nu deține `unfiltered_html`, așa că WordPress îți trece conținutul prin
filtrul lui KSES.

- Trimitem în mod deliberat **niciun** bloc `<style>` — WordPress elimină
  tagurile, dar lasă CSS-ul în urmă ca text vizibil, ceea ce ar arunca o
  foaie de stil în articolul tău
- SVG e eliminat complet, deci graficele și diagramele dispar
- Textul, titlurile, listele, tabelele, codul, structura de callout-uri tot ajung
:::

::::

Dashboard-ul te avertizează despre asta după un test de conexiune, ca să
afli înainte să afle cititorii tăi. Pe WordPress **multisite**, doar un
Super Admin deține `unfiltered_html` — asta e o regulă WordPress, nu a
noastră.

## Câmpuri SEO (Yoast și Rank Math)

Trimitem fraza cheie țintă, titlul SEO și meta descrierea, atât pentru Yoast
SEO, cât și pentru Rank Math, la fiecare trimitere. Dacă rămân sau nu ține
de site-ul tău:

:::callout{severity=warning title="Niciunul dintre plugin-uri nu acceptă astea prin REST, implicit"}
Câmpurile lui Yoast sunt meta „protejat" în WordPress, iar API-ul lui e
doar-citire; cele ale Rank Math sunt pur și simplu neînregistrate pentru
REST. WordPress nu respinge chei meta necunoscute — le **elimină silențios**.
Citim înapoi răspunsul propriu al WordPress ca să vedem ce chiar s-a
stocat, deci nu pretindem niciodată că o frază cheie a ajuns când nu a
ajuns.
:::

Ca să rămână, pune acest plugin must-use în
`wp-content/mu-plugins/baas-seo-bridge.php` — byte-identic cu fișierul pe
care stack-ul compose al acestui proiect îl montează în propria țintă de
dogfood WordPress:

```php
<?php
/**
 * Plugin Name: baas SEO bridge
 * Description: Exposes the Yoast SEO and Rank Math post-meta fields over the
 *              WordPress REST API so an external publisher can set the focus
 *              keyphrase, SEO title, and meta description when it pushes a
 *              post. Without this, WordPress silently drops those keys. Also
 *              registers the _baas_post_id ownership marker so a retried push
 *              recognises a post it already created and updates it in place
 *              instead of publishing a duplicate — and never touches a post it
 *              did not create.
 * Version:     1.1.0
 * License:     MIT
 *
 * Drop this file in wp-content/mu-plugins/ — must-use plugins load
 * automatically and cannot be deactivated by accident.
 */

defined('ABSPATH') || exit;

add_action('init', function () {
    $keys = [
        // Yoast SEO. Leading underscore = protected meta, which the REST API
        // refuses to touch unless it is registered with an auth_callback.
        '_yoast_wpseo_focuskw',
        '_yoast_wpseo_metadesc',
        '_yoast_wpseo_title',
        // Rank Math. Not underscore-prefixed, but still unregistered for REST
        // on a stock install, so it needs the same treatment.
        'rank_math_focus_keyword',
        'rank_math_description',
        'rank_math_title',
        // baas ownership marker: our own post uuid, written on create and read
        // back (context=edit) so an id-less retry adopts only the post WE made.
        '_baas_post_id',
    ];

    foreach ($keys as $key) {
        register_post_meta('post', $key, [
            'type'          => 'string',
            'single'        => true,
            'show_in_rest'  => true,
            'auth_callback' => function ($allowed, $meta_key, $post_id) {
                // Same gate the editor screen uses: whoever may edit the post
                // may set its SEO fields.
                return current_user_can('edit_post', $post_id);
            },
        ]);
    }
});

// Keep the ownership marker out of PUBLIC (view-context) REST responses. We
// only ever read it back in edit context (authenticated) to recognise a post
// we created; exposing it to anonymous callers would leak our internal post
// id and baas provenance on the customer's own public site.
add_filter('rest_prepare_post', function ($response, $post, $request) {
    if (($request['context'] ?? 'view') !== 'edit') {
        $data = $response->get_data();
        if (isset($data['meta']['_baas_post_id'])) {
            unset($data['meta']['_baas_post_id']);
            $response->set_data($data);
        }
    }
    return $response;
}, 10, 3);
```

Tot restul — titlu, slug, conținut, rezumat, dată de publicare, status —
funcționează pe WordPress standard, fără niciun plugin.

:::callout{severity=info title="Nu suprascriem niciodată URL-ul canonical"}
Odată ce un articol trăiește pe WordPress-ul tău, acolo ar trebui să-l
găsească cititorii și crawlerele, așa că îi lăsăm pe Yoast și Rank Math
să-și seteze singuri canonical-ul. Dacă *și* încorporezi același blog pe
site-ul tău cu [JS SDK-ul](/ro/docs/install-sdk), alege-l tu însuți pe unul
dintre cele două ca și canonical — două articole identice la două URL-uri e
singurul lucru pe care motoarele de căutare nu-l agreează aici.
:::

## Sau trage articolele cu plugin-ul

Dacă preferi să nu ne dai o parolă de aplicație, instalează plugin-ul
**DailySmith Sync** și lasă WordPress-ul să tragă el articolele. Interoghează
[fluxul de sincronizare](/ro/docs/delivery-api) din oră în oră cu cheia publică a
site-ului tău, creează sau actualizează articolele cu același conținut,
câmpuri SEO, imagine reprezentativă și stilizare pe care le trimite și push-ul,
și nu suprascrie niciodată un articol pe care nu l-a creat el. Înregistrează
și câmpurile SEO și marcajul de proprietate de mai sus, deci cu el instalat nu
mai ai nevoie de snippet-ul bridge, iar un site care rulează ambele canale
ajunge la un singur articol WordPress per articol.

Plugin-ul e open source (MIT) și stă pe GitHub:
[TechnicalInsideTI/dailysmith-wordpress-plugin](https://github.com/TechnicalInsideTI/dailysmith-wordpress-plugin).
Descarcă arhiva zip din ultima versiune publicată, încarc-o din **Module →
Adaugă modul nou**, apoi deschide **Setări → DailySmith Sync** și lipește
cheia `pk_…`.

## Depanare

| Ce vezi | Ce înseamnă |
|---|---|
| `401 (incorrect_password)` | Parola de aplicație e greșită sau a fost revocată. Generează una nouă — valoarea veche e irecuperabilă. |
| `401` cu o parolă corectă | Serverul tău elimină headerul `Authorization`. Pe Apache, adaugă `RewriteRule .* - [E=REMOTE_USER:%{HTTP:Authorization}]` chiar după `RewriteEngine On` în `.htaccess`. |
| `403 (rest_cannot_create)` | Utilizatorul s-a autentificat, dar nu are voie să creeze articole. Folosește un editor sau un administrator. |
| `404` pe ruta REST | Permalinkurile prietenoase sunt oprite, sau un plugin de securitate a dezactivat API-ul REST. Setează **Setări → Permalinkuri** la orice altceva decât „Simplu". |
| „returned something that is not JSON" | URL-ul nu e rădăcina site-ului — folosește `https://example.com`, nu `https://example.com/wp-admin`. |
| „Application passwords are not available" | WordPress oferă parole de aplicație doar peste **HTTPS**. Pune site-ul în spatele TLS. |
| Stilizare lipsă pe WordPress | Utilizatorul conectat nu deține `unfiltered_html` — vezi **Ce supraviețuiește trimiterii** mai sus. |

## Note de securitate

- Parola de aplicație e stocată **criptată la repaus** (AES-256-GCM, cu un
  nonce proaspăt per valoare, iar textul cifrat legat de blogul căruia îi
  aparține). Nu e niciodată returnată de vreun răspuns API, niciodată
  logată și niciodată randată în dashboard.
- O parolă de aplicație **nu** e autentificarea ta WordPress. Revocarea ei
  din **Utilizatori → Profil** ne taie accesul imediat și nu schimbă nimic
  altceva.
- Dă-i cel mai mic rol care poate publica. Editor e de obicei potrivit;
  administrator e necesar doar dacă vrei și câmpuri SEO la nivel de plugin,
  setate de un utilizator care poate instala fragmentul-punte.
- Cerem `https://` pentru URL-ul site-ului. O parolă de aplicație trimisă
  peste HTTP simplu poate fi citită de orice se află pe traseu.

## Notă pentru auto-găzduire

Operatorii care își rulează propria instanță baas trebuie să seteze
`BAAS_ENCRYPTION_KEY` la 32 de octeți aleatori, codificați base64
(`openssl rand -base64 32`); fără ea, API-ul refuză să stocheze
credențiale WordPress. Ca să rotești: mută valoarea curentă în
`BAAS_ENCRYPTION_KEYS_OLD` (separate prin virgulă, doar pentru
decriptare), pune o cheie nouă în `BAAS_ENCRYPTION_KEY` și repornește.
Credențialele stocate continuă să funcționeze pentru că vechea cheie încă
le decriptează — dar **nu** sunt re-criptate automat. Ca să retragi complet
cheia veche, re-salvează fiecare conexiune WordPress (asta o rescrie cu
cheia nouă), apoi elimină cheia retrasă din `BAAS_ENCRYPTION_KEYS_OLD`.
