{
  "name": "R2 SEO",
  "slug": "r2-seo",
  "version": "1.7.4",
  "requires": "6.5",
  "tested": "6.7",
  "requires_php": "8.1",
  "author": "<a href='https://rise2.studio'>RISE2 Studio</a>",
  "author_profile": "https://rise2.studio",
  "homepage": "https://rise2.studio",
  "last_updated": "2026-09-06",
  "download_url": "https://plugins.rise2.studio/r2-seo/r2-seo-1.7.4.zip",
  "sections": {
    "description": "<p><strong>R2 SEO is an add-on for Slim SEO</strong>, not a replacement. Slim SEO stores one title and description per post-type archive for the whole site, so on a multilingual project one language's text ends up on every language's archive. This add-on adds a field per language for each context Slim SEO keeps global, and feeds the value back through Slim SEO's own filters.</p><h4>What it covers</h4><ul><li>Post-type archives, taxonomy archives, the blog home, search results and the 404 page</li><li>Title, meta description and og:image, one field per active language</li><li>WPML first class (Polylang supported far enough not to break); works on a single-language site too</li></ul><h4>Fallback</h4><p>Our per-language field &rarr; WPML String Translation (optional, off by default) &rarr; whatever Slim SEO would have produced. Where we have nothing, Slim SEO's value is returned untouched &mdash; so <strong>deactivating the add-on returns the site to plain Slim SEO with nothing left behind in the HTML</strong>, and uninstalling deletes only this add-on's two options.</p><p>Requires Slim SEO 4.10.0 or newer; below that it registers no filters and shows an admin notice.</p>",
    "changelog": "<h4>1.7.4</h4>\n<ul>\n<li>Fixed a false positive in the page check: alt=\"\" is a deliberate decorative marker, not a missing alt. Only a missing alt attribute, or a file name used as one, is reported now.</li>\n</ul>\n<h4>1.7.3</h4>\n<ul>\n<li>Saving any R2 SEO screen now purges the page cache — without it a cached site kept serving the old &lt;head&gt; and the settings looked like they had done nothing.</li>\n<li>llms.txt entries now cut their note at 200 characters.</li>\n</ul>\n<h4>1.7.2</h4>\n<ul>\n<li>Fixed: the launch checks' \"Measured …\" note and Check again button were missing on the first view of the Coverage tab.</li>\n</ul>\n<h4>1.7.1</h4>\n<ul>\n<li>Security: llms.txt no longer lists password-protected posts or post types set to noindex, and ?r2seo_llms=1 can no longer hijack other URLs.</li>\n<li>Security: the ACF description fallback no longer publishes a password-protected post's content.</li>\n<li>Fixed: the description fallback never fired on a normal page — Slim SEO passes its unrendered auto-description template, not an empty string.</li>\n<li>Fixed: a redirect CSV mixing absolute and relative URLs for one path wrote a rule that redirected the URL to itself.</li>\n<li>Fixed: a CSV without an object_type column silently wrote term rows onto unrelated posts.</li>\n<li>Fixed: duplicate rows for one object corrupted the undo snapshot.</li>\n<li>Fixed: the spreadsheet-formula guard's apostrophe was written back into Slim SEO's meta on re-import.</li>\n<li>Fixed: the self-hosted updater was constructed with the wrong key, so updates never appeared and every request logged a PHP warning.</li>\n<li>Fixed: launch checks now cached for ten minutes instead of making blocking loopback requests on every render.</li>\n<li>Fixed: the hand-written llms.txt lost percent-encoded characters on save.</li>\n<li>Fixed: deactivating left the /llms.txt rewrite rule behind.</li>\n<li>Fixed: lazy-load placeholders doubled the reported missing-alt count.</li>\n</ul>\n<h4>1.7.0</h4>\n<ul>\n<li>Meta CSV round-trip: export every page and term, fill it in outside WordPress, import it back into Slim SEO's own fields. Empty cells never clear a value, every import previews first, and the last one can be undone.</li>\n<li>Coverage report and launch checks: the title and description Slim SEO would actually output for every object, flagged, with a CSV export and a bounded page check for H1s, hreflang and image alt.</li>\n<li>llms.txt, off by default, generated per language and served from a rewrite rule.</li>\n<li>Redirect import from a plain from,to CSV into Slim SEO Redirection, with validation.</li>\n<li>Fallbacks: per-page social image from a nominated ACF gallery, after Slim SEO's own chain, and a description composed from ACF fields for pages whose post content is empty.</li>\n<li>Fixed: /llms.txt no longer redirects to /llms.txt/, and its output no longer carries HTML entities.</li>\n<li>Fixed: a redirect preview no longer over-counts a source URL listed twice in the same file.</li>\n<li>Fixed: og:image dimensions are suppressed when the fallback supplies the image, not only the per-language override.</li>\n</ul>\n<h4>1.6.0</h4><ul><li><strong>Ispravljeno: izvoz je pokrivao samo jezik admina.</strong> WPML sužava <code>get_posts()</code> na aktivni jezik, pa je izvoz trojezične stranice davao trećinu redaka &mdash; i pritom izgledao potpuno. Na pilot stranici 6 umjesto 18. Izvoz sada prolazi kroz sve aktivne jezike i skuplja po ID-u posta, uz vraćanje izvornog jezika; Polylang to rješava jednim upitom. Svaki redak sada nosi i <code>lang</code>, da se zna koji jezični stupac vrijedi za taj post.</li></ul><h4>1.5.1</h4><ul><li><strong>Ispravljeno: <code>containedInPlace</code> je pokazivao na čvor koji schema.org odbija.</strong> To svojstvo očekuje <code>Place</code>, a uz entitet ograničen na naslovnicu (zadano) čvor iza tog <code>@id</code> je na svakoj drugoj stranici Slimov obični <code>Organization</code> — koji nije Place. validator.schema.org je javljao <code>INVALID_OBJECT</code> na svakoj stranici jedinice, dok je naslovnica bila čista, pa se golim okom nije vidjelo. Sada taj čvor ondje dobiva tip samog entiteta (npr. Resort), <strong>samo tip</strong> — bez adrese, geo i kontakata, da ne postane cijeli entitet ponovljen po podstranicama.</li></ul><h4>1.5.0</h4><ul><li><strong>Entitet stranice dobiva <code>numberOfRooms</code> i popis sadržaja po jeziku.</strong> Uočeno pri čitanju ručno pisanog bloka na pilot stranici prije zamjene &mdash; nosio je oboje, pa bi ga uklanjanje tiho odnijelo (<code>numberOfRooms: 12</code> i četiri <code>amenityFeature</code>). Sadržaji koriste isti <code>LocationFeatureSpecification</code> oblik kao i entiteti sadržaja; oboje se izostavlja kad je prazno.</li></ul><h4>1.4.0</h4><ul><li><strong><code>containsPlace</code> na entitetu stranice</strong> &mdash; sažet popis mapiranih stavki, uz postavku gdje se prikazuje: naslovnica (zadano), naslovnica i arhive, ili nigdje. Sam se izostavlja kad ništa nije mapirano.</li><li>Vraća ono što je davao ručno pisani blok, bez onoga što je s njim bilo krivo: taj se popis ponavljao na <em>svakoj</em> podstranici. Ovdje je vezan uz <strong>stranicu</strong>, a ne uz opseg entiteta, pa i kad je entitet postavljen na cijeli site popis ostaje izvan podstranica.</li><li>Stavke su samostalni objekti s vlastitim <code>@id</code> &mdash; istim onim koji puni čvor koristi na svojoj stranici &mdash; pa se prepoznaju kao isti entitet bez viseće reference. Svaka nosi i svoj razriješeni tip, pa je vila <code>House</code> a suite <code>Suite</code>, umjesto da su sve spljoštene u jedan tip kao u ručnoj verziji.</li></ul><h4>1.3.0</h4><ul><li><strong>Tip scheme po pojedinom postu.</strong> Mapiranje po tipu sadržaja je ispravno za većinu stranica, ali pregrubo kad jedan CPT drži stvarno različite stvari &mdash; na pilot stranici isti <code>accommodation</code> pokriva vilu (<code>House</code>), dva suitea (<code>Suite</code>) i tri apartmana (<code>Apartment</code>). Nametnuti im jedan tip bila bi tvrdnja za koju znamo da nije točna.</li><li><strong>Provjereno na pilot stranici od početka do kraja</strong> (Slim SEO 4.10.0, WPML EN/HR/DE, 18 objavljenih jedinica). Prije: dva odvojena bloka, ručno pisani <code>Resort</code> ponovljen na svakoj podstranici, jedinice bez vlastitog entiteta. Poslije: <strong>jedan</strong> blok, <code>Resort</code> JEST <code>#organization</code> pa <code>about</code> i <code>publisher</code> vode na pun entitet, svaka jedinica ima svoj čvor s <code>containedInPlace</code> i <code>mainEntity</code>, na jeziku stranice.</li></ul><h4>1.2.0</h4><ul><li><strong>Schema po sadržaju (sloj B).</strong> Tip sadržaja se mapira na schema tip, pa se vrijednosti popunjavaju po postu iz <strong>šablone</strong> &mdash; ne čitanjem iz ACF-a. Čvor ulazi u Slimov <code>@graph</code>, a <code>WebPage.mainEntity</code> pokazuje na njega.</li><li><strong>Zašto šablona, a ne mapiranje na ACF:</strong> pilot stranica nema <em>nijedno</em> strukturirano polje za kvadraturu, kapacitet, sobe ni kupaonice &mdash; sve je slobodan tekst na četiri nesinkronizirana mjesta po postu &times; tri jezika. Mapiranje bi značilo parsere po jeziku, izrađene iznova na svakom projektu i lomljive na svaku izmjenu teksta.</li><li><strong>JSON izvoz/uvoz</strong> &mdash; izvezeš datoteku sa svim svojstvima i jezicima, popuniš je izvan WordPressa (ručno ili AI-jem) i uvezeš natrag. <strong>Plugin sam ništa ne zove i ništa ne šalje van</strong> (spec §4 / D-02). Prazna vrijednost pri uvozu znači &bdquo;ne diraj&ldquo;, nikad &bdquo;obriši&ldquo;. Uvoz ima pregled prije spremanja.</li><li><code>containedInPlace</code> se veže na entitet iz sloja A <strong>bez ijednog dodatnog polja</strong>, i to samo kad taj čvor stvarno postoji na toj stranici &mdash; viseća <code>@id</code> referenca se ne može dogoditi.</li><li><strong>Oblici vrijednosti provjereni na izvorima:</strong> <code>floorSize</code> = QuantitativeValue + MTK; <code>occupancy</code> = QuantitativeValue <em>bez</em> unitCode (schema.org i Google si proturječe oko C62/IE); <code>numberOfBathroomsTotal</code> = goli Integer; <code>typeOfBed</code> je zatvorena lista pa se nepoznat tip odbacuje; prazna svojstva se izostavljaju umjesto da se šalju prazna ili izmišljaju kao nula.</li></ul><h4>1.1.0</h4><ul><li><strong>Entitet stranice (schema sloj A).</strong> Tip, naziv i opis <strong>po jeziku</strong>, NAP, geo, karta, telefon, e-mail, cjenovni rang, logo i <code>sameAs</code>.</li><li><strong>Dva načina subjekta.</strong> <em>Jedan</em> &mdash; pravna osoba i objekt su isti subjekt, jedan čvor (avara). <em>Odvojeno</em> &mdash; operater <code>#organization</code> i objekt <code>#place</code>, povezani u oba smjera preko <code>subOrganization</code>/<code>parentOrganization</code>. Način je postavka, pa idući projekt ne traži izmjenu koda.</li><li><strong>Ne stvara se drugi otok.</strong> Slim SEO već ispisuje <code>Organization</code> s <code>@id</code> = <code>{home}#organization</code> i već na njega veže <code>WebSite.publisher</code> i <code>WebPage.about</code>. Zato se ne dodaje drugi entitet nego se <strong>spaja u postojeći čvor</strong> i nadograđuje mu <code>@type</code> &mdash; jedan graf, veze ostaju.</li><li><strong>Opseg prikaza</strong>, po defaultu samo naslovnica. To je ono što sprječava da se cijeli entitet ponavlja na svakoj podstranici.</li><li>Prazna polja se izostavljaju, a ne šalju kao prazni stringovi; <code>geo</code> se sprema samo kad su obje koordinate tu; bez konfiguracije graf prolazi potpuno netaknut.</li></ul><h4>1.0.0</h4><ul><li><strong>Prva verzija &mdash; meta po jeziku</strong> za sve kontekste koje Slim SEO drži globalnima: arhive svakog CPT-a, taksonomijske arhive, blog naslovnicu, rezultate pretrage i 404. Naslov, meta opis i og:image, po jedno polje za svaki aktivni jezik.</li><li><strong>Lanac fallbacka:</strong> naše polje za taj jezik &rarr; WPML String Translation (opcionalno, isključeno po defaultu) &rarr; ono što bi Slim SEO ionako ispisao. Ondje gdje nemamo ništa, Slimova vrijednost prolazi netaknuta &mdash; zato deaktivacija vraća stranicu na čisti Slim SEO bez ostataka u HTML-u.</li><li><strong>Ispravak specifikacije:</strong> hook <code>slim_seo_meta_facebook_image</code> <em>ne postoji</em> u Slim SEO 4.10.0. og:image ide kroz dinamički <code>slim_seo_open_graph_image</code>. Uz to, Slim SEO izvede <code>og:image:width/height/alt</code> iz iste, jednom razriješene slike &mdash; pa se ta tri taga brišu kad podmetnemo svoju sliku, da ne opisuju tuđu.</li><li><strong>WPML String Translation s izričitim izvornim jezikom.</strong> Registracija bez njega pusti WPML da pogodi (obično engleski), a string čiji se zabilježeni izvorni jezik ne poklapa sa sadržajem <em>nikad</em> se ne može prevesti.</li></ul>"
  }
}
