#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""StudioSoft: artikel baru — laporan SEO bulanan, angka yang wajib ada (pilar C retainer).

Jalur: python3 ss_artikel_laporan_seo_bulanan.py            (dry run: hitung + cek)
       python3 ss_artikel_laporan_seo_bulanan.py --draft    (buat DRAFT + meta Yoast)
       python3 ss_artikel_laporan_seo_bulanan.py --jadwal   (set status=future 2026-10-20 08:00)
"""
import base64, json, re, sys, time, urllib.error, urllib.request

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

JUDUL = "Laporan SEO Bulanan: Angka Apa Saja yang Wajib Ada"
SLUG = "laporan-seo-bulanan-angka-wajib"
FOKUS = "laporan seo bulanan"
META_JUDUL = "Laporan SEO Bulanan: Angka yang Wajib Ada"
META_DESC = ("Laporan SEO bulanan yang berguna memuat empat kelompok angka: kunjungan pencarian, kontak "
             "masuk, kondisi teknis, dan biaya per calon pembeli.")
KATEGORI = [330]          # SEO
TANGGAL = "2026-10-20T08:00:00"   # zona situs (Asia/Jakarta)

WA = ("https://wa.me/6281935000624?text=Halo%20StudioSoft%2C%20saya%20ingin%20konsultasi%20"
      "digital%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>')

ISI = """
<p>Setiap bulan laporan datang, biasanya berisi tabel angka dan grafik garis yang naik turun. Anda
membaca sekilas, mengangguk, lalu menyimpannya ke folder. Beberapa minggu kemudian muncul pertanyaan
yang sebenarnya sederhana: dari semua angka itu, mana yang menandakan pekerjaan SEO berjalan, dan mana
yang cuma naik karena musim.</p>

<p>Laporan SEO bulanan yang berguna tidak perlu tebal. Yang penting adalah angkanya bisa dijelaskan,
saling terhubung, dan mengarah ke satu kesimpulan: pekerjaan bulan ini memberi hasil atau tidak, dan
bulan depan sebaiknya diapakan. Berikut empat kelompok angka yang wajib ada, cara membacanya, serta
tanda laporan yang hanya tampak sibuk.</p>

<h2>Kenapa laporan SEO bulanan sering terasa kosong</h2>
<p>Masalah pertama biasanya bukan datanya, melainkan urutannya. Angka disusun mengikuti menu alat
analitik: sesi, pengguna, tayangan halaman, durasi kunjungan. Susunan seperti itu menjawab pertanyaan
alat, bukan pertanyaan pemilik bisnis yang ingin tahu apakah uangnya bekerja.</p>

<p>Masalah kedua adalah angka tanpa pembanding. Seratus pengunjung tidak berarti apa-apa kalau tidak
ada angka bulan lalu, angka periode yang sama tahun lalu, atau target yang disepakati. Angka baru punya
arti ketika bisa diletakkan di sebelah angka lain.</p>

<p>Laporan yang sehat karena itu selalu memasangkan angka dengan penjelasannya. Kalau tayangan naik,
dari kueri mana kenaikannya datang. Kalau kontak masuk turun, bagian mana dari proses yang berubah.
Tanpa penjelasan sebab, angka hanya menambah panjang laporan tanpa menambah pemahaman.</p>

<p>Lima tanda berikut biasanya cukup untuk mengenali laporan yang hanya tampil sibuk:</p>
<ul>
<li>Semua angkanya naik, tetapi tidak ada satu pun yang bisa Anda buka sendiri.</li>
<li>Tidak ada bagian yang menyebut pekerjaan yang gagal atau percobaan yang dihentikan.</li>
<li>Tanggal laporan berpindah-pindah tanpa alasan yang dijelaskan.</li>
<li>Rekomendasi berupa daftar pekerjaan tanpa menyebut halaman sasaran dan perkiraan hasilnya.</li>
<li>Tidak ada keputusan yang diminta dari Anda di akhir laporan.</li>
</ul>

<h2>Angka pertama: kunjungan dari pencarian dan kueri yang bergerak</h2>
<p>Pisahkan pengunjung dari pencarian dan pengunjung dari iklan. Laporan yang mencampur keduanya
membuat Anda sulit menilai pekerjaan SEO, karena lonjakan iklan bisa menutupi penurunan pencarian.</p>

<p>Setelah itu, lihat jumlah kueri yang memunculkan situs Anda. Angka ini sering dilewatkan padahal
bergerak lebih dulu. Halaman Anda biasanya mulai muncul untuk banyak kueri baru sebelum peringkatnya
naik ke posisi yang menghasilkan klik. Kabar baik di dua bulan pertama umumnya terlihat di sini, bukan
di jumlah pengunjung.</p>

<p>Angka berikutnya di kelompok ini adalah jumlah halaman yang menerima kunjungan. Kalau hanya beranda
dan dua halaman lama yang ramai, artinya konten baru belum bekerja. Sebaran kunjungan yang melebar
menandakan situs mulai ditemukan lewat berbagai topik.</p>

<h2>Angka kedua: kontak masuk dan klik WhatsApp</h2>
<p>Kunjungan bukan hasil akhir. Yang menentukan bagi bisnis adalah berapa orang yang menghubungi.
Karena itu, pasang pelacakan pada tombol WhatsApp, formulir, dan nomor telepon, lalu laporkan jumlahnya
setiap bulan.</p>

<p>Angka ini perlu dipecah menurut halaman asalnya. Kalau mayoritas kontak datang dari dua artikel
lama, tim konten sebaiknya belajar dari halaman itu dan membuat tulisan serupa. Sebaliknya, halaman
yang banyak dikunjungi tetapi tidak menghasilkan kontak biasanya punya masalah pada ajakan atau pada
kecocokan topik dengan pembacanya.</p>

<p>Sebutkan juga tingkat konversi sederhana, yaitu jumlah kontak dibagi jumlah kunjungan. Angka ini
memudahkan Anda menilai apakah kenaikan kunjungan benar-benar berguna atau hanya menambah lalu lintas
yang lewat begitu saja.</p>

<p>Kalau pelacakan klik WhatsApp belum dipasang, satu-satunya cara menghitung kontak adalah bertanya
ke tim yang menjawab percakapan. Cara itu masih berguna, tetapi hasilnya kasar dan mudah terlewat saat
percakapan menumpuk di akhir bulan. Menyiapkan pelacakan sekali di awal jauh lebih hemat waktu
dibanding menghitung manual setiap bulan.</p>

<h2>Angka ketiga: halaman terindeks dan kecepatan situs</h2>
<p>Bagian teknis cukup satu blok pendek. Laporkan jumlah halaman yang terindeks, jumlah halaman yang
ditolak, dan alasan penolakannya. Kenaikan halaman terindeks yang sehat biasanya diikuti kenaikan
tayangan beberapa minggu kemudian.</p>

<p>Untuk kecepatan, pakai satu angka tetap, misalnya waktu muat di ponsel untuk halaman yang paling
penting. Membandingkan angka yang sama dari bulan ke bulan lebih berguna daripada menyajikan hasil
beberapa alat sekaligus yang satu sama lain tidak bisa dibandingkan.</p>

<h2>Angka keempat: biaya per calon pembeli</h2>
<p>Kalau ada anggaran iklan atau biaya pengelolaan, hitung berapa rupiah yang dikeluarkan untuk
mendapatkan satu calon pembeli. Angka ini menyatukan sisi biaya dan sisi hasil, sehingga mudah dipakai
saat memutuskan penambahan atau pemotongan anggaran bulan berikutnya.</p>

<p>Bandingkan juga biaya tersebut antara kanal. Pencarian organik biasanya lebih murah per calon
pembeli dibanding iklan, tetapi butuh waktu lebih panjang. Menyajikan keduanya berdampingan membuat
keputusan soal kecepatan dan biaya jadi lebih masuk akal.</p>

<h2>Cara membaca laporan tiga bulan pertama</h2>
<p>Bulan pertama umumnya dihabiskan untuk perbaikan teknis, riset kata kunci, dan pemasangan
pelacakan. Angka yang masuk akal untuk dilihat di sini adalah jumlah halaman yang diperbaiki dan
jumlah kueri yang mulai muncul, bukan jumlah pengunjung.</p>

<p>Bulan kedua mulai terlihat pergerakan kueri dan halaman baru yang menerima kunjungan. Bulan ketiga
adalah waktu yang tepat untuk menilai gabungan kunjungan, kontak masuk, dan biaya per calon pembeli,
lalu memutuskan anggaran periode berikutnya dari angka tersebut.</p>

<p>Menjelang akhir bulan ketiga, mintalah tiga hal secara tertulis: halaman mana yang memberi kunjungan
paling banyak, kueri mana yang tumbuh paling cepat, dan kanal mana yang paling mahal per calon pembeli.
Jawaban atas ketiga hal itu biasanya sudah cukup untuk menentukan arah periode berikutnya tanpa perlu
membaca ulang seluruh isi laporan.</p>

<h2>Pertanyaan yang sering diajukan</h2>

<h3>Berapa lama sampai laporan pertama menunjukkan hasil</h3>
<p>Untuk pekerjaan SEO, tiga bulan adalah batas pertama yang wajar untuk menilai arah. Hasil yang bisa
dikaitkan langsung dengan nilai penjualan biasanya baru muncul setelah periode itu, karena halaman yang
baru terindeks masih perlu waktu untuk naik ke posisi yang menghasilkan klik.</p>

<h3>Kalau angkanya turun, apakah berarti pengelolanya salah</h3>
<p>Belum tentu. Penurunan bisa datang dari perubahan algoritme, musim, atau pesaing yang menambah
anggaran. Yang menentukan adalah apakah penyebabnya disebutkan dan langkah perbaikannya dijelaskan.
Laporan yang rapi akan menunjukkan bagian mana yang turun, dugaan penyebabnya, dan rencana bulan
berikutnya.</p>

<h3>Apakah dasbor otomatis dari alat sudah cukup</h3>
<p>Cukup sebagai lampiran, tidak cukup untuk mengambil keputusan. Dasbor menjawab pertanyaan apa yang
terjadi, sementara keputusan butuh jawaban soal kenapa dan selanjutnya apa. Dua hal itu biasanya masih
perlu ditulis oleh orang yang mengerjakan.</p>

<h2>Langkah berikutnya</h2>
<p>Kalau Anda sedang menerima laporan SEO bulanan, coba periksa apakah empat kelompok angka di atas ada
semuanya. Bagian yang kosong itulah yang sebaiknya Anda tanyakan lebih dulu pada pertemuan berikutnya,
lengkap dengan contoh halaman yang menurut Anda penting tetapi tidak muncul di laporan.</p>

<p>Untuk membandingkan berapa biaya yang wajar dan apa saja yang biasanya masuk paket, lihat rincian
<a href="https://studiosoft.id/jasa-seo-surabaya-paket-harga/">paket dan harga jasa SEO Surabaya</a>.
Kami juga pernah memeriksa ratusan halaman milik klien dan menuangkan temuannya dalam
<a href="https://studiosoft.id/audit-290-artikel-halaman-yatim/">audit 290 artikel</a>, yang menunjukkan
bagian mana yang paling sering luput dari perhatian. Kalau Anda perlu panduan memilih penyedia, ada
penjelasan di <a href="https://studiosoft.id/cara-memilih-jasa-seo-surabaya/">cara memilih jasa SEO
Surabaya</a>.</p>

""" + TOMBOL + """
<p><em>Catatan: daftar angka ini kami pakai untuk menyusun laporan klien kami sendiri. Kalau ada bagian
yang belum pernah masuk laporan Anda, sebutkan saja, itu lebih cepat ditangani daripada dibiarkan.</em></p>
"""

SCHEMA = """
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[
{"@type":"Question","name":"Berapa lama sampai laporan pertama menunjukkan hasil","acceptedAnswer":{"@type":"Answer","text":"Untuk pekerjaan SEO, tiga bulan adalah batas pertama yang wajar untuk menilai arah. Hasil yang bisa dikaitkan langsung dengan nilai penjualan biasanya baru muncul setelah periode itu, karena halaman yang baru terindeks masih perlu waktu untuk naik ke posisi yang menghasilkan klik."}},
{"@type":"Question","name":"Kalau angkanya turun, apakah berarti pengelolanya salah","acceptedAnswer":{"@type":"Answer","text":"Belum tentu. Penurunan bisa datang dari perubahan algoritme, musim, atau pesaing yang menambah anggaran. Yang menentukan adalah apakah penyebabnya disebutkan dan langkah perbaikannya dijelaskan. Laporan yang rapi akan menunjukkan bagian mana yang turun, dugaan penyebabnya, dan rencana bulan berikutnya."}},
{"@type":"Question","name":"Apakah dasbor otomatis dari alat sudah cukup","acceptedAnswer":{"@type":"Answer","text":"Cukup sebagai lampiran, tidak cukup untuk mengambil keputusan. Dasbor menjawab pertanyaan apa yang terjadi, sementara keputusan butuh jawaban soal kenapa dan selanjutnya apa. Dua hal itu biasanya masih perlu ditulis oleh orang yang mengerjakan."}}]}
</script>
"""
ISI_FULL = ISI + SCHEMA

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(S + 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 as e:
            terakhir = str(e)[:120]
            time.sleep(4)
    return 0, terakhir


def polos(html):
    t = re.sub(r"<script.*?</script>", " ", html, flags=re.S | re.I)
    t = re.sub(r"<[^>]+>", " ", t)
    return re.sub(r"\s+", " ", t).strip()


if __name__ == "__main__":
    teks = polos(ISI_FULL)
    kata = len(teks.split())
    internal = re.findall(r'href="(https://studiosoft\.id/[^"]+)"', ISI_FULL)
    print(f"judul    : {JUDUL} ({len(JUDUL)} char)")
    print(f"slug     : {SLUG}")
    print(f"meta jdl : {META_JUDUL} ({len(META_JUDUL)} char)")
    print(f"meta desc: {len(META_DESC)} char · mulai dgn fokus: {META_DESC.lower().startswith(FOKUS)}")
    print(f"panjang  : {kata} kata (baku 1.000-1.300)")
    print(f"h2       : {ISI_FULL.count('<h2>')} · h3: {ISI_FULL.count('<h3>')}")
    print(f"internal : {len(internal)} {internal}")
    print(f"tombol WA: {'ada' if 'Konsultasi Gratis' in ISI_FULL and 'wa.me/6281935000624' in ISI_FULL else 'TIDAK ADA'}")
    print(f"FAQPage  : {'ada' if 'FAQPage' in ISI_FULL else 'TIDAK ADA'}")
    print(f"fokus kw di ISI: {ISI_FULL.lower().count(FOKUS)}x")

    if "--draft" not in sys.argv and "--jadwal" not in sys.argv:
        print("\n(dry run)")
        sys.exit(0)

    st, cek = rest(f"/wp-json/wp/v2/posts?slug={SLUG}&status=any&_fields=id,status")
    if isinstance(cek, list) and cek:
        pid = cek[0]["id"]
        print(f"\n   sudah ada #{pid} status={cek[0]['status']} -> lanjut pakai id itu")
    else:
        st, d = rest("/wp-json/wp/v2/posts", "POST", {
            "title": JUDUL, "slug": SLUG, "content": ISI_FULL, "status": "draft",
            "comment_status": "closed", "categories": KATEGORI, "excerpt": META_DESC})
        if not isinstance(d, dict) or "id" not in d:
            print(f"   X gagal buat draft: http={st} {str(d)[:300]}")
            sys.exit(1)
        pid = d["id"]
        print(f"\n   OK DRAFT #{pid} -> {d.get('link')} status={d.get('status')}")

    st, d2 = rest(f"/wp-json/wp/v2/posts/{pid}", "POST", {"meta": {
        "_yoast_wpseo_title": META_JUDUL, "_yoast_wpseo_metadesc": META_DESC,
        "_yoast_wpseo_focuskw": FOKUS}})
    print(f"   meta: http={st}")

    st, v = rest(f"/wp-json/wp/v2/posts/{pid}?context=edit")
    if isinstance(v, dict):
        m = v.get("meta") or {}
        isi_raw = v["content"]["raw"]
        print(f"   baca balik: status={v['status']} tanggal={v['date']} slug={v['slug']} kategori={v.get('categories')}")
        print(f"   meta tersimpan: fokus='{m.get('_yoast_wpseo_focuskw')}' "
              f"judul='{m.get('_yoast_wpseo_title')}' desc={len(m.get('_yoast_wpseo_metadesc') or '')} char")
        print(f"   isi: {len(isi_raw)} char · {len(polos(isi_raw).split())} kata · "
              f"wa.me={'ya' if 'wa.me/6281935000624' in isi_raw else 'TIDAK'} · "
              f"FAQPage={'ya' if 'FAQPage' in isi_raw else 'TIDAK'}")
    else:
        print(f"   X baca balik gagal: {v}")

    if "--jadwal" in sys.argv:
        st, d3 = rest(f"/wp-json/wp/v2/posts/{pid}", "POST", {"status": "future", "date": TANGGAL})
        print(f"\n   set future: http={st} status={(d3 or {}).get('status') if isinstance(d3, dict) else d3}")
        st, v2 = rest(f"/wp-json/wp/v2/posts/{pid}?context=edit&_fields=id,status,date,date_gmt,slug,link")
        print(f"   baca balik: {json.dumps(v2, ensure_ascii=False)}")
        json.dump({"id": pid, "slug": SLUG, "status": (v2 or {}).get("status"),
                   "date": (v2 or {}).get("date"), "kata": kata, "internal": internal},
                  open(f"{WS}/ss_artikel_laporan_seo_hasil.json", "w"), ensure_ascii=False, indent=1)
        print("   -> ss_artikel_laporan_seo_hasil.json")
