#!/usr/bin/env python3
"""StudioSoft: artikel harian 17 Okt 2026 (pilar B AI) — struktur URL dan kategori.
Pola mengikuti ss_artikel_kata_kunci_lokal.py. Tahap: draf dulu (--apply), baru dijadwalkan (--jadwalkan TANGGAL).
Catatan: satu elemen blok = satu baris. Jangan melipat baris di dalam <p>/<li>, karena wpautop mengubahnya jadi <br>.
"""
import base64, json, re, sys, time, urllib.error, urllib.request

WS = "/home/mantapman/.hermes/profiles/atun/workspace"
DOM = "studiosoft.id"
AUTH = base64.b64encode(b"bambangbot:q26r RePo BVy6 pwU2 F3z7 ouyl").decode()

JUDUL = "Struktur URL dan Kategori: Kerapian yang Berpengaruh ke Peringkat"
SLUG = "struktur-url-dan-kategori"
FOKUS = "struktur url"
META_JUDUL = "Struktur URL dan Kategori: Panduan Menata Situs"
META_DESC = ("Struktur URL dan kategori yang rapi membuat Google lebih mudah memahami halaman Anda. "
             "Ikuti urutan membenahi slug dan kategori tanpa merusak peringkat lama.")

KAT = [330]  # SEO

WA = "https://wa.me/6281935000624?text=Halo%20StudioSoft%2C%20saya%20ingin%20konsultasi%20digital%20marketing"
TOMBOL = ('<p style="text-align:center;margin:32px 0 8px"><a href="' + WA +
          '" target="_blank" rel="noopener" style="display:inline-block;background-color:#0f2557;'
          'color:#ffffff;padding:15px 34px;font-size:18px;font-weight:bold;text-decoration:none;'
          'border-radius:6px;">Konsultasi Gratis</a></p>')

L1 = "https://studiosoft.id/10-checklist-audit-seo-gratis-yang-bikin-website-umkm-naik-peringkat-dalam-7-hari/"
L2 = "https://studiosoft.id/google-search-console-untuk-pemula-7-manfaat-wajib-untuk-website-bisnis-naik-peringkat-google/"
L3 = "https://studiosoft.id/perbedaan-onpage-offpage-seo/"
L4 = "https://studiosoft.id/jasa-seo-surabaya-paket-harga/"

FAQ = [
    ("Apakah mengubah alamat artikel akan menghilangkan peringkatnya?",
     "Tidak, selama alamat lamanya tidak dibiarkan mati. Yang merugikan bukan perubahan alamatnya, "
     "tetapi pengunjung yang mendapati alamat lama berakhir tanpa arahan. Kalau alamat lama mengarah ke "
     "alamat baru dan tautan internal ikut diperbarui, halaman itu tetap dikenali sebagai halaman "
     "yang sama."),
    ("Berapa jumlah kategori yang wajar untuk situs bisnis?",
     "Sebanyak kelompok yang benar-benar dibutuhkan pengunjung. Untuk situs layanan, tiga sampai lima "
     "kategori biasanya sudah cukup, dan sisanya lebih tepat disimpan sebagai tag. Kategori yang "
     "jumlahnya jauh lebih banyak daripada kebutuhan pengunjung hanya menambah halaman tipis."),
    ("Kalau alamat artikel saya masih berisi tanggal, apakah harus diganti sekarang?",
     "Periksa dulu apakah halaman itu menerima tayangan dan tautan masuk. Halaman yang sudah bekerja "
     "sebaiknya dibiarkan. Halaman yang tidak pernah muncul di pencarian bisa dirapikan sekalian saat "
     "isinya diperbarui, supaya Anda tidak mengubah alamat dua kali."),
    ("Apakah halaman kategori perlu diberi deskripsi?",
     "Perlu, kalau halaman itu memang ingin muncul di hasil pencarian. Kalimat pengantar yang menjelaskan "
     "isi kelompok membuat halaman kategori punya nilai sendiri. Kalau tidak ada pengantar yang bisa "
     "ditulis, lebih baik halaman itu disembunyikan dari hasil pencarian."),
    ("Seberapa besar pengaruh struktur alamat dibandingkan isi artikel?",
     "Isi tetap penentu utama. Kerapian alamat dan kategori lebih berperan sebagai pendukung: halaman "
     "lebih cepat dipahami, dan halaman Anda tidak lagi saling mengambil posisi untuk kueri yang sama."),
]

ISI = """
<p>Ketika kami memeriksa situs klien, bagian yang paling sering berantakan bukan isi artikelnya, melainkan alamat halaman dan penataan kategori. Artikel sudah panjang dan relevan, tetapi alamatnya masih berbentuk /?p=1473, sementara beberapa kategori dipakai untuk kueri yang mirip. Dari luar situs terlihat rapi, tetapi bagi mesin pencari situs itu sulit dibaca.</p>

<p>Kerapian ini jarang diurus karena tidak ada angka yang bisa dipamerkan setelahnya. Padahal dampaknya terasa di banyak hal sekaligus: halaman lebih cepat dipahami, tautan internal lebih mudah disusun, dan halaman Anda tidak lagi bersaing dengan halaman Anda sendiri. Menata keduanya bisa dimulai dari situs yang baru, dan situs lama yang sudah punya peringkat tetap bisa dirapikan asal urutannya benar.</p>

<h2>Alamat halaman adalah sinyal yang paling sering diabaikan</h2>
<p>Alamat halaman memberi tahu dua hal kepada mesin pencari: topik halaman dan seberapa jauh halaman itu dari beranda. Kata di dalam alamat ikut dibaca, sama seperti kata di judul dan isi. Jumlah tingkat folder juga memberi petunjuk apakah halaman ini pendukung atau halaman utama.</p>

<p>Yang lebih penting bagi bisnis Anda adalah stabilitas. Alamat yang sering berubah membuat tautan dari situs lain dan tautan internal di artikel Anda sendiri menunjuk ke tempat yang salah. Setiap perubahan alamat sebaiknya keputusan sadar, bukan efek samping dari penataan yang terburu-buru.</p>

<h2>Aturan slug untuk artikel baru</h2>
<p>Bagian ini paling mudah dikerjakan karena masih bisa diputuskan sebelum menulis. Beberapa aturan yang kami pakai untuk artikel klien.</p>

<ul>
<li>Pakai huruf kecil semua. Huruf besar di alamat membuat tautan dari luar mudah salah tulis dan tidak konsisten.</li>
<li>Pisahkan kata dengan tanda minus, bukan garis bawah. Tanda minus dibaca sebagai pemisah kata, garis bawah dibaca sebagai satu kata panjang.</li>
<li>Ambil dua sampai lima kata inti. Alamat yang terlalu panjang sulit dibaca dan mudah terpotong di layar ponsel.</li>
<li>Hindari angka tanggal di dalam alamat. Artikel yang diperbarui tiap tahun akan memaksa Anda mengganti alamat setiap kali diperbarui.</li>
<li>Hindari kata sambung berlebihan seperti dan, atau, atau untuk, karena tidak menambah makna.</li>
<li>Satu alamat untuk satu topik. Dua artikel dengan maksud sama tetapi alamat berbeda adalah masalah, bukan tambahan halaman.</li>
</ul>

<p>Aturan ini bagian dari pekerjaan optimasi on page, yang kami bahas terpisah di <a href=\"""" + L3 + """\">perbedaan on page dan off page SEO</a>. Kalau terpaksa mengubah alamat artikel yang sudah tayang, lakukan sekaligus sampai tuntas: alamat lama harus mengarah ke alamat baru, dan tautan internal yang menunjuk alamat lama ikut diperbarui.</p>

<h2>Kategori: kapan membantu, kapan jadi pesaing</h2>
<p>Kategori berguna ketika artikel sudah banyak dan pengunjung butuh cara cepat menemukan tulisan sejenis. Di titik itu, kategori berfungsi seperti daftar isi. Masalahnya muncul ketika kategori dibiarkan tampil sebagai halaman biasa, dengan judul dan deskripsi yang hampir sama dengan artikel di dalamnya.</p>

<p>Tiga aturan praktis yang kami pakai sebelum menambah kategori baru.</p>

<ul>
<li>Tambahkan kategori jika ada minimal tiga artikel yang benar-benar membahas kelompok itu. Di bawah jumlah itu, kategori hanya menambah halaman tipis.</li>
<li>Satu artikel masuk ke satu kategori utama, bukan ke banyak kategori sekaligus. Kategori yang diisi berulang-ulang kehilangan fungsinya sebagai pengelompok.</li>
<li>Beri halaman kategori kalimat pengantar asli, bukan daftar judul saja. Halaman kelompok yang kosong lebih baik tidak ditampilkan di hasil pencarian.</li>
</ul>

<p>Halaman label dan arsip tanggal biasanya tidak perlu muncul di hasil pencarian karena isinya berulang dengan daftar artikel. Banyak pengaturan SEO sederhana sudah menyediakan pilihan untuk menyembunyikannya, dan menyembunyikannya sering kali menaikkan kualitas halaman yang benar-benar Anda bidik.</p>

<h2>Kanibalisasi halaman, dan cara mengenalinya dari data</h2>
<p>Kanibalisasi terjadi ketika dua halaman atau lebih bersaing untuk kueri yang sama. Gejalanya terlihat di laporan kinerja pencarian: satu kueri memunculkan dua alamat berbeda dari situs Anda, dan posisi keduanya naik turun bergantian. Trafik dari kueri itu seret, padahal halamannya sebenarnya layak naik.</p>

<p>Cara memeriksanya sederhana. Ambil satu kueri yang penting bagi bisnis Anda, lalu lihat alamat mana saja yang muncul untuk kueri itu. Kalau ada lebih dari satu alamat dari situs Anda, itu tanda benturan. Periksa apakah yang bertabrakan adalah artikel dengan halaman kategori, dua artikel dengan sudut mirip, atau halaman layanan dengan artikel pendukung.</p>

<p>Penyelesaiannya bukan menghapus, tetapi menentukan mana yang menjadi rujukan utama. Halaman dengan tayangan dan tautan terbanyak biasanya yang dipilih. Isi baru dari halaman lain dipindahkan lebih dulu, baru halaman itu diarahkan ke rujukan tersebut.</p>

<h2>Urutan merapikan situs lama tanpa kehilangan peringkat</h2>
<p>Bagian yang paling ditakuti pemilik situs adalah merapikan alamat yang sudah lama tayang. Urutannya seperti ini.</p>

<ol>
<li>Buat daftar semua halaman: alamat, jumlah kata, dan data kinerjanya di pencarian. Kerjakan dari data, bukan dari perasaan bahwa satu alamat terlihat jelek.</li>
<li>Tandai halaman yang alamatnya benar-benar perlu berubah, yaitu alamat yang berisi nomor, salah ejaan, atau tidak menggambarkan isinya. Alamat yang hanya kurang cantik tetapi sudah berperingkat sebaiknya dibiarkan.</li>
<li>Ubah satu kelompok kecil dalam satu waktu, misalnya sepuluh halaman, supaya dampaknya bisa Anda lihat sebelum melanjutkan.</li>
<li>Pastikan alamat lama mengarah ke alamat baru. Kalau situs Anda memakai WordPress dengan struktur tautan tetap, pengalihan ini otomatis. Kalau tidak, pengalihan perlu dipasang manual.</li>
<li>Perbarui tautan internal di artikel lain yang masih menunjuk alamat lama, supaya pengunjung tidak melewati pengalihan yang tidak perlu.</li>
<li>Periksa satu sampai dua pekan setelah perubahan: halaman baru harus terindeks, dan alamat lama harus mengarah dengan benar. Kalau ada yang jatuh, kembalikan satu langkah.</li>
</ol>

<p>Yang tidak perlu dilakukan: mengganti alamat beranda, mengganti alamat halaman layanan yang sudah berperingkat, dan merapikan seluruh situs dalam satu malam.</p>

<h2>Pemeriksaan satu jam yang bisa Anda kerjakan sendiri</h2>
<p>Kalau ingin tahu kondisi situs Anda sekarang, sediakan satu jam dan kerjakan urutan ini.</p>

<ol>
<li>Buka <a href=\"""" + L2 + """\">Search Console</a> dan pilih menu kinerja. Ambil lima kueri dengan tayangan terbesar, lalu catat berapa alamat situs Anda yang muncul untuk masing-masing kueri.</li>
<li>Cari nama domain Anda di Google untuk melihat halaman mana yang sudah terindeks.</li>
<li>Periksa daftar kategori di situs Anda. Hitung ada berapa kategori, dan berapa artikel pada masing-masing kategori.</li>
<li>Amati tiga alamat artikel terbaru. Sudahkah alamatnya memakai kata yang orang cari, tanpa tanggal dan tanpa nomor?</li>
</ol>

<p>Catat temuan itu di satu lembar, lalu kerjakan dari yang paling mudah dan tidak mengubah alamat. Untuk pemeriksaan yang lebih rinci, lihat <a href=\"""" + L1 + """\">checklist audit SEO gratis</a> yang kami pakai saat memeriksa situs baru.</p>

<h2>Pertanyaan yang sering diajukan</h2>
<h3>""" + FAQ[0][0] + """</h3>
<p>""" + FAQ[0][1] + """</p>
<h3>""" + FAQ[1][0] + """</h3>
<p>""" + FAQ[1][1] + """</p>
<h3>""" + FAQ[2][0] + """</h3>
<p>""" + FAQ[2][1] + """</p>
<h3>""" + FAQ[3][0] + """</h3>
<p>""" + FAQ[3][1] + """</p>
<h3>""" + FAQ[4][0] + """</h3>
<p>""" + FAQ[4][1] + """</p>

<p>Struktur alamat dan kategori bukan pekerjaan sekali jadi, tetapi juga tidak rumit. Mulailah dari halaman yang paling sering dikunjungi, tentukan satu halaman rujukan untuk setiap kueri penting, lalu rapikan alamat yang memang menyesatkan. Kalau Anda memerlukan pemeriksaan menyeluruh pada situs lama, kami biasa mulai dari mencatat seluruh alamat dan kategori, lalu menyusun urutan perbaikan yang risikonya paling kecil. Cakupan pekerjaan dan kisaran biayanya ada di <a href=\"""" + L4 + """\">halaman jasa SEO Surabaya</a>.</p>

""" + TOMBOL + """
<p><em>Catatan: urutan pemeriksaan di atas kami pakai saat memeriksa situs klien sendiri, bukan disusun dari contoh di buku panduan.</em></p>
"""

LD = {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": [{"@type": "Question", "name": q,
                    "acceptedAnswer": {"@type": "Answer", "text": a}} for q, a in FAQ],
}
ISI = ISI + '\n<script type="application/ld+json">' + json.dumps(LD, ensure_ascii=False) + "</script>\n"

UA = {"User-Agent": "Mozilla/5.0"}


def rest(p, method="GET", data=None):
    h = {"Authorization": f"Basic {AUTH}", **UA}
    body = None
    if data is not None:
        body = json.dumps(data).encode()
        h["Content-Type"] = "application/json"
    r = urllib.request.Request(f"https://{DOM}{p}", data=body, headers=h, method=method)
    for _ in range(3):
        try:
            with urllib.request.urlopen(r, timeout=180) as x:
                return x.status, json.loads(x.read())
        except urllib.error.HTTPError as e:
            return e.code, e.read().decode()[:300]
        except Exception:
            time.sleep(4)
    return 0, "gagal"


tanpa_script = re.sub(r"<script.*?</script>", " ", ISI, flags=re.S)
kata_visible = len(re.sub(r"<[^>]+>", " ", tanpa_script).split())
kata_hitung = len(re.sub(r"<[^>]+>", " ", ISI).split())
patah = [x for x in re.findall(r"<(?:p|li|h2|h3)>(.*?)</(?:p|li|h2|h3)>", ISI, re.S)
         if re.search(r"<", x) is None and "\n" in x.strip() and not x.strip().endswith((".", "!", "?", ":", ";"))]
print("=== artikel baru:", JUDUL)
print(f"   panjang naskah: {kata_visible} kata terlihat · {kata_hitung} kata versi cek_pola_ai (baku 1.000-1.300)")
print(f"   meta judul: {len(META_JUDUL)} char (maks 60)")
print(f"   meta desc : {len(META_DESC)} char (140-158) | fokus di awal: {META_DESC.lower().startswith(FOKUS)}")
print(f"   tombol WA : {'ada' if WA in ISI else 'TIDAK ADA'}")
print(f"   tautan internal: {len(re.findall(r'href=.https://studiosoft.id/', ISI))}")
print(f"   h2: {len(re.findall('<h2', ISI))} | h3: {len(re.findall('<h3', ISI))} | FAQPage: {'FAQPage' in ISI}")
print(f"   patahan baris di dalam elemen: {len(patah)}")

if "--apply" not in sys.argv and "--jadwalkan" not in sys.argv:
    print("\n(dry run — pakai --apply untuk bikin draf, --jadwalkan YYYY-MM-DD untuk menjadwalkan)")
    sys.exit(0)

if "--apply" in sys.argv:
    st, d = rest(f"/wp-json/wp/v2/posts?slug={SLUG}&_fields=id,slug,status")
    if isinstance(d, list):
        for p in d:
            print(f"   ⚠ slug {SLUG} sudah dipakai: #{p['id']} [{p['status']}] — hentikan, ganti slug")
        if d:
            sys.exit(1)
    st, d = rest("/wp-json/wp/v2/posts", "POST", {
        "title": JUDUL, "slug": SLUG, "content": ISI, "status": "draft",
        "categories": KAT, "comment_status": "closed",
        "meta": {"_yoast_wpseo_title": META_JUDUL, "_yoast_wpseo_metadesc": META_DESC,
                 "_yoast_wpseo_focuskw": FOKUS}})
    if not isinstance(d, dict) or "id" not in d:
        print("   ❌ gagal bikin draf:", st, str(d)[:300])
        sys.exit(1)
    pid = d["id"]
    print(f"\n   ✅ draf #{pid} → {d.get('link')}")
    st2, d2 = rest(f"/wp-json/wp/v2/posts/{pid}?context=edit")
    raw = (d2.get("content") or {}).get("raw", "")
    meta = (d2.get("meta") or {})
    print(f"   baca balik: status={d2.get('status')} slug={d2.get('slug')} kata="
          f"{len(re.sub(r'<[^>]+>', ' ', raw).split())} kategori={d2.get('categories')}")
    print("   meta: " + json.dumps({k: v for k, v in meta.items() if "yoast" in k}, ensure_ascii=False)[:400])
    simpan = {"id": pid, "judul": JUDUL, "slug": SLUG, "tautan": d.get("link")}
    json.dump(simpan, open(f"{WS}/drafts/ss_artikel_struktur_url.json", "w"), ensure_ascii=False, indent=1)
    print("   → id disimpan di drafts/ss_artikel_struktur_url.json")

if "--jadwalkan" in sys.argv:
    tgl = sys.argv[sys.argv.index("--jadwalkan") + 1]
    pid = json.load(open(f"{WS}/drafts/ss_artikel_struktur_url.json"))["id"]
    st, d = rest(f"/wp-json/wp/v2/posts/{pid}", "POST",
                 {"status": "future", "date": f"{tgl}T08:00:00"})
    if not isinstance(d, dict) or d.get("status") != "future":
        print("   ❌ gagal menjadwalkan:", st, str(d)[:300])
        sys.exit(1)
    st2, d2 = rest(f"/wp-json/wp/v2/posts/{pid}?context=edit&_fields=id,status,date,slug,content,meta")
    raw = (d2.get("content") or {}).get("raw", "")
    print(f"   ✅ dijadwalkan #{pid}: status={d2.get('status')} date={d2.get('date')} slug={d2.get('slug')}")
    print(f"   isi: {len(re.sub(r'<[^>]+>', ' ', raw).split())} kata | tombol WA: "
          f"{'ada' if 'wa.me/6281935000624' in raw else 'TIDAK ADA'} | tautan internal: "
          f"{len(re.findall(r'href=.https://studiosoft.id/', raw))}")
