#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""StudioSoft: artikel baru — audit SEO teknis, 12 pemeriksaan dan urutannya (pilar A uang).

Jalur: python3 ss_artikel_audit_seo_teknis.py            (dry run: hitung + cek)
       python3 ss_artikel_audit_seo_teknis.py --draft    (buat DRAFT + meta Yoast)
       python3 ss_artikel_audit_seo_teknis.py --jadwal   (set status=future 2026-10-21 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 = "Audit SEO Teknis: 12 Pemeriksaan dan Urutannya"
SLUG = "audit-seo-teknis-12-pemeriksaan"
FOKUS = "audit seo teknis"
META_JUDUL = "Audit SEO Teknis: 12 Pemeriksaan dan Urutannya"
META_DESC = ("Audit SEO teknis memeriksa 12 hal menurut urutan tertentu, dari indeksasi sampai "
             "kecepatan dan pengukuran. Ini daftar pemeriksaannya beserta alasannya.")
KATEGORI = [330]          # SEO
TANGGAL = "2026-10-21T08: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>Bayangkan Anda meminta audit SEO, lalu menerima berkas tiga puluh halaman berisi tangkapan layar
dari enam alat berbeda. Ada catatan tentang deskripsi halaman yang terlalu panjang, ada grafik
kecepatan, ada daftar judul yang belum dirapikan. Semuanya benar. Setelah membacanya, Anda masih belum
tahu pekerjaan mana yang harus dikerjakan lebih dulu.</p>

<p>Sumber masalahnya biasanya bukan alat, melainkan urutan. Audit SEO teknis yang berguna disusun
seperti pemeriksaan kesehatan: hal yang bisa membatalkan seluruh temuan lain diperiksa paling awal.
Kalau separuh halaman tidak terindeks, memperbaiki deskripsi halaman yang tidak pernah muncul di
pencarian hanya membuang waktu dan biaya. Dua belas pemeriksaan di bawah ini kami susun mengikuti
urutan tersebut, dengan alasan kenapa sebuah langkah harus didahulukan.</p>

<p>Pemeriksaan teknis bersifat berantai, dan satu temuan sering membuat temuan lain tidak lagi
relevan. Halaman yang diblokir di robots.txt tidak akan pernah muncul, sepanjang apa pun isinya. Situs
yang lambat membuat pengunjung pergi sebelum satu tombol pun ditekan. Karena itu urutan yang benar
bergerak dari yang menghalangi, ke yang menghambat, lalu ke yang menghaluskan.</p>

<p>Urutan yang salah meninggalkan dua jejak yang mudah dikenali. Laporan penuh catatan kecil yang
tidak berdampak ke jumlah kunjungan. Dan temuan besar luput, karena perhatian sudah habis dipakai
memeriksa hal sepele di beranda.</p>

<h2>Pemeriksaan 1 sampai 3: apakah halaman bisa ditemukan</h2>
<p>Grup pertama menentukan apakah halaman Anda punya peluang masuk ke hasil pencarian. Semua temuan di
grup berikutnya hanya berlaku kalau tiga hal ini sudah beres.</p>

<p>Pertama, status indeksasi. Buka laporan halaman di Google Search Console, lalu bandingkan jumlah
halaman yang terindeks dengan jumlah halaman yang seharusnya ada. Selisihnya adalah daftar pekerjaan
paling awal. Halaman yang ditolak perlu dibaca satu per satu bersama alasan penolakannya, karena
penolakan bisa berarti duplikat, bisa juga berarti halaman sengaja disembunyikan.</p>

<p>Kedua, berkas pengarah mesin pencari. Periksa robots.txt, sitemap.xml, dan tag kanonik yang
tertanam di halaman. Kesalahan kecil di sini mahal harganya. Satu baris aturan yang salah bisa menarik
satu kategori halaman keluar dari pencarian tanpa pesan error yang muncul di mana pun.</p>

<p>Ketiga, kode respons dan pengalihan. Kumpulkan semua alamat yang membalas kode selain 200, lalu
rapikan. Rantai pengalihan yang berlapis-lapis memboroskan kesempatan dan memecah sinyal yang dikirim
ke mesin pencari.</p>

<h2>Pemeriksaan 4 sampai 6: apakah mesin paham isi halaman</h2>
<p>Setelah halaman bisa ditemukan, pertanyaannya berubah menjadi apakah Google tahu halaman itu
membahas apa.</p>

<p>Pemeriksaan keempat adalah struktur judul. Satu H1 untuk setiap halaman, H2 yang menggambarkan
bagiannya, dan urutan yang masuk akal. Situs yang berkembang selama beberapa tahun sering punya judul
bertumpuk karena dikerjakan oleh orang yang berbeda pada masa yang berbeda.</p>

<p>Pemeriksaan kelima adalah data terstruktur. Penanda organisasi, layanan, produk, dan tanya jawab
membantu mesin memahami konteks halaman, termasuk saat jawabannya diambil untuk asisten AI. Periksa
hasilnya lewat alat penguji, jangan bersandar pada asumsi bahwa plugin sudah mengurus semuanya.</p>

<p>Pemeriksaan keenam adalah isi yang bertumpuk. Dua halaman dengan maksud kueri yang sama akan saling
menekan di hasil pencarian. Tandanya mudah dikenali: judul yang hampir sama, dan bagian isi yang
disalin apa adanya. Salah satu halaman perlu dipilih sebagai rujukan, sisanya diarahkan ke halaman
itu. Kami pernah memetakan masalah sejenis pada ratusan halaman dan menuangkannya di
<a href="https://studiosoft.id/audit-290-artikel-halaman-yatim/">temuan audit 290 artikel</a>.</p>

<h2>Pemeriksaan 7 sampai 9: apakah halaman cepat dibuka</h2>
<p>Kecepatan diperiksa setelah indexasi dan struktur, bukan sebelum. Alasannya sederhana: halaman
yang tidak terindeks tidak akan pernah mendapat manfaat dari perbaikan kecepatan.</p>

<p>Pemeriksaan ketujuh adalah tiga angka inti pengalaman halaman: waktu muat elemen terbesar, jeda
respons saat pengunjung menekan tombol, dan pergeseran tata letak saat halaman dibuka. Ukur di
halaman yang paling penting bagi penjualan, bukan hanya di beranda. Penjelasan tiap angka beserta
ambangnya ada di <a href="https://studiosoft.id/core-web-vitals-kecepatan-website-2026/">panduan Core
Web Vitals</a>.</p>

<p>Pemeriksaan kedelapan adalah gambar. Banyak situs menyimpan foto berukuran beberapa ribu piksel
untuk ditampilkan sebesar kartu nama. Ukuran berkas, format, dan ukuran tampilan yang tertulis di
kode semuanya berpengaruh.</p>

<p>Pemeriksaan kesembilan adalah skrip, huruf, dan simpanan sementara. Daftar semua berkas yang
dimuat halaman, tandai yang tidak terpakai, dan pastikan halaman publik disajikan dari simpanan
sementara alih-alih dirakit ulang setiap kali ada kunjungan.</p>

<h2>Pemeriksaan 10 sampai 12: apakah halaman saling terhubung dan terukur</h2>
<p>Grup terakhir ini yang paling sering dilewatkan, padahal murah dikerjakan.</p>

<p>Pemeriksaan kesepuluh adalah tautan internal. Setiap halaman penting sebaiknya punya tautan masuk
dari halaman lain, bukan hanya dari menu. Halaman yang tidak punya satu pun tautan masuk hampir tidak
pernah ditemukan pembaca maupun mesin pencari.</p>

<p>Pemeriksaan kesebelas adalah tautan yang rusak. Telusuri semua tautan keluar dan tautan antar
halaman, catat yang membalas kode 404 atau mengarah ke domain mati. Untuk situs yang rajin menulis,
pemeriksaan ini sebaiknya dijalankan berkala, bukan sekali setahun.</p>

<p>Pemeriksaan kedua belas adalah pengukuran. Pastikan pelacakan kunjungan, tombol WhatsApp, formulir,
dan nomor telepon benar-benar tercatat, dan pastikan alat ukur hanya dipasang sekali. Tanpa pengukuran
yang bersih, Anda tidak akan pernah tahu apakah perbaikan teknis tadi berdampak.</p>

<h2>Tanda laporan audit yang layak dipercaya</h2>
<p>Lima tanda berikut membedakan laporan audit SEO teknis yang bisa dipakai untuk mengambil keputusan
dari laporan yang hanya tampak rapi:</p>
<ul>
<li>Setiap temuan menyebut alamat halaman yang bermasalah, bukan hanya jenis masalahnya.</li>
<li>Ada urutan perbaikan dengan alasan, sehingga Anda tahu mana yang mendesak.</li>
<li>Ada perkiraan dampak dan tingkat kesulitan untuk tiap temuan.</li>
<li>Ada bagian berisi hal yang sudah benar, supaya Anda tidak memperbaiki sesuatu yang sebenarnya
sehat.</li>
<li>Angka yang dilaporkan bisa Anda buka sendiri di akun analitik milik bisnis Anda.</li>
</ul>

<h2>Cara menyusun urutan perbaikan setelah audit</h2>
<p>Setelah audit SEO teknis selesai, jangan mengerjakan daftar temuan dari yang paling mudah.
Kerjakan dari yang memblokir, lalu dari yang berdampak ke banyak halaman sekaligus.</p>

<p>Perbaikan borongan biasanya jauh lebih hemat. Kalau empat puluh halaman memakai templat yang sama
dan judulnya sama-sama terlalu panjang, satu perubahan di templat menyelesaikan keempat puluhnya
sekaligus. Menyunting satu per satu di editor hanya masuk akal untuk halaman yang benar-benar
istimewa, misalnya halaman layanan utama.</p>

<p>Beri jeda sebelum menilai hasilnya. Perubahan teknis butuh waktu untuk dirayapi ulang, dan
pergerakan peringkat biasanya baru terlihat dua sampai empat minggu setelah perbaikan tayang. Yang
layak dipantau di masa jeda itu adalah jumlah halaman yang terindeks dan jumlah kueri yang mulai
memunculkan situs Anda.</p>

<h2>Pertanyaan yang sering diajukan</h2>

<h3>Apakah audit teknis bisa dikerjakan sendiri tanpa alat berbayar</h3>
<p>Bisa, dan sebagian besar memang cukup memakai alat gratis milik Google: Search Console untuk
indexasi dan kueri, PageSpeed Insights untuk kecepatan, serta pemeriksa data terstruktur. Alat
berbayar lebih membantu ketika halaman Anda sudah ratusan dan pemeriksaan perlu dijalankan otomatis
setiap bulan.</p>

<h3>Berapa lama waktu yang dibutuhkan untuk satu audit</h3>
<p>Untuk situs berisi puluhan halaman, penelusuran dan penyusunan laporan biasanya memakan waktu
beberapa hari kerja. Situs besar dengan ribuan halaman butuh lebih lama di tahap pengelompokan, karena
temuan harus diringkas per templat agar tidak menjadi daftar yang tak terbaca.</p>

<h3>Kalau tidak ditemukan masalah teknis, apa artinya</h3>
<p>Artinya perbaikan tidak lagi bisa diharapkan dari sisi teknis, dan pekerjaan berikutnya bergerak ke
sisi isi serta ketepatan sasaran. Sering kali temuan seperti itu justru kabar baik, karena berarti
anggaran bisa dialihkan ke produksi konten atau ke halaman layanan yang berpeluang menghasilkan
kontak.</p>

<h2>Langkah berikutnya</h2>
<p>Kalau situs Anda sudah berjalan lebih dari dua tahun dan belum pernah diperiksa dari sisi teknis,
mulailah dari tiga pemeriksaan pertama di atas. Bagian itu bisa Anda kerjakan sendiri hari ini dengan
akun yang sudah ada, dan hasilnya biasanya cukup untuk menentukan perlu tidaknya bantuan lanjutan.</p>

<p>Untuk gambaran pekerjaan teknis berkala, lihat
<a href="https://studiosoft.id/jasa-maintenance-wordpress-surabaya/">jasa maintenance WordPress
Surabaya</a> yang memuat pemeriksaan rutin bulanan. Kalau Anda ingin tahu apa saja yang diperiksa
beserta kisaran biayanya, rinciannya ada di
<a href="https://studiosoft.id/jasa-seo-surabaya-paket-harga/">paket dan harga jasa SEO Surabaya</a>.</p>

""" + TOMBOL + """
<p><em>Catatan: urutan dua belas pemeriksaan ini kami pakai saat mengaudit situs klien sendiri. Kalau
ada bagian yang belum pernah diperiksa di situs 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":"Apakah audit teknis bisa dikerjakan sendiri tanpa alat berbayar","acceptedAnswer":{"@type":"Answer","text":"Bisa, dan sebagian besar memang cukup memakai alat gratis milik Google: Search Console untuk indexasi dan kueri, PageSpeed Insights untuk kecepatan, serta pemeriksa data terstruktur. Alat berbayar lebih membantu ketika halaman Anda sudah ratusan dan pemeriksaan perlu dijalankan otomatis setiap bulan."}},
{"@type":"Question","name":"Berapa lama waktu yang dibutuhkan untuk satu audit","acceptedAnswer":{"@type":"Answer","text":"Untuk situs berisi puluhan halaman, penelusuran dan penyusunan laporan biasanya memakan waktu beberapa hari kerja. Situs besar dengan ribuan halaman butuh lebih lama di tahap pengelompokan, karena temuan harus diringkas per templat agar tidak menjadi daftar yang tak terbaca."}},
{"@type":"Question","name":"Kalau tidak ditemukan masalah teknis, apa artinya","acceptedAnswer":{"@type":"Answer","text":"Artinya perbaikan tidak lagi bisa diharapkan dari sisi teknis, dan pekerjaan berikutnya bergerak ke sisi isi serta ketepatan sasaran. Sering kali temuan seperti itu justru kabar baik, karena berarti anggaran bisa dialihkan ke produksi konten atau ke halaman layanan yang berpeluang menghasilkan kontak."}}]}
</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)
    terakhir = "gagal"
    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_audit_seo_hasil.json", "w"), ensure_ascii=False, indent=1)
        print("   -> ss_artikel_audit_seo_hasil.json")
