بنقرة واحدة
awcms-micro-blog-content
Kerjakan bagian mana pun dari epic blog_content AWCMS-Micro (Issue
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Kerjakan bagian mana pun dari epic blog_content AWCMS-Micro (Issue
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Triase dan perbaiki temuan CodeQL code scanning AWCMS-Micro (github.com/ahliweb/awcms-micro/security/code-scanning). Gunakan saat diminta "analisis code scanning"/"perbaiki CodeQL", saat sebuah PR gagal check CodeQL, atau saat menemukan alert baru. Mendokumentasikan enam false-positive nyata yang sudah ditemukan (name-heuristic password, incompatible-types typeof/null, URL substring-sanitization di test mock, dua kasus dismiss resmi tanpa reformulasi kode, Bun.SQL tagged-template null-cast, dan build-time extension seam trivial-conditional) plus pola "unused-local-variable di test kadang menandai coverage gap" — supaya tidak diinvestigasi ulang dari nol.
Scaffold modul baru pada modular monolith AWCMS-Micro. Gunakan saat membuat modul domain baru di src/modules/ (mis. warehouse-management, accounting-tax) atau saat memerlukan struktur module.ts + domain/application/infrastructure/api + README. Ikuti struktur standar doc 10 & 11.
Terapkan kontrol akses RBAC+ABAC default-deny plus RLS pada endpoint/service AWCMS-Micro. Gunakan pada setiap endpoint non-public dan setiap akses data tenant-scoped. Menegakkan default deny, deny-overrides-allow, decision log, dan tenant isolation sesuai doc 03 & 10.
Tulis audit log untuk aksi high-risk AWCMS-Micro dengan redaction. Gunakan pada login, access assignment, profile merge, price change, transaction posted/cancel/return, stock adjustment, warehouse transfer, Coretax export, sync conflict resolution, AI tool call, dan security readiness decision. Sesuai doc 03 & 10.
Terapkan idempotency pada mutation high-risk AWCMS-Micro agar aman dari double-submit. Gunakan saat implementasi posting transaksi, cancel/return, transfer approve/ship/receive, cycle count, adjustment, VAT generate, Coretax batch, receipt send, sync push, atau workflow decision. Sesuai doc 10.
Kelola/konsumsi sistem Module Management AWCMS-Micro (registry, komposisi modul build-time untuk aplikasi turunan, manifest kompatibilitas aplikasi turunan, tenant lifecycle enable/disable, settings, permission sync/status, navigation, job registry, health/readiness). Gunakan saat menambah field descriptor baru (permissions/navigation/settings/jobs/health) di modul lain, saat menyelidiki kenapa suatu modul terlihat degraded/orphaned, saat aplikasi turunan perlu menyusun modulnya sendiri lewat `src/modules/application-registry.ts` tanpa mengedit registry base, saat aplikasi turunan perlu memverifikasi kompatibilitasnya dengan rilis base terbaru (`bun run extension:check`), atau saat mengubah perilaku enable/disable/settings/health module_management sendiri. Sesuai src/modules/module-management/README.md, epic
| name | awcms-micro-blog-content |
| description | Kerjakan bagian mana pun dari epic blog_content AWCMS-Micro (Issue |
blog_content (src/modules/blog-content) adalah modul domain pertama
yang didaftarkan langsung di repo base ini (epic #536, bukan di aplikasi
turunan terpisah — lihat AGENTS.md §Peta modul dan
docs/adr/0009-public-tenant-scoped-routes.md). Epic #536 (Issue #537-#543)
sudah selesai — modul terdaftar status: "active" (bukan lagi
experimental). Skill ini merangkum keputusan yang sudah dibuat supaya
issue susulan (di luar epic ini) wajib memakai ulang, bukan
mendesain ulang — baca src/modules/blog-content/README.md untuk detail
lengkap tiap tabel dan endpoint.
Skill ini melengkapi (bukan menggantikan) awcms-micro-new-endpoint,
awcms-micro-new-migration, awcms-micro-testing, dll. — itu tetap dipakai
untuk cara membangun endpoint/migration/test. Skill ini menyediakan
konteks domain blog_content spesifik yang tidak jelas dari sekadar
membaca satu file migration.
| Issue | Scope | Status |
|---|---|---|
| #537 | Schema, domain validation, permission seed | Selesai (migration 026/027) |
| #538 | Admin API + lifecycle actions (posts) | Selesai (/api/v1/blog/posts, lihat README) |
| #539 | Pages, taxonomi, post-term relation, search | Selesai (/api/v1/blog/pages, /terms, /search) |
| #540 | Rute publik, RSS, sitemap, SEO | Selesai (/blog/{tenantCode}/..., lihat README) |
| #541 | Revisions + scheduled publishing + AsyncAPI | Selesai (/posts/{id}/revisions, blog:publish:scheduled, lihat README) |
| #542 | Template/menu/widget/media/multilingual/ads | Selesai (/templates, /menus, /widgets, /ads, /theme, lihat README) |
| #543 | Admin UI, blog settings API, dokumentasi akhir, hardening | Selesai (14 layar /admin/blog/..., /api/v1/blog/settings, lihat README) |
Di luar epic #536: blog_content sekarang juga punya rute publik kedua,
/news/... (Issue #560, epic #555 "online public tenant routing") — lihat
§Rute publik di bawah dan skill awcms-micro-tenant-domain-routing's §Rute
publik /news untuk detail resolusi tenant lintas-mode. /blog/{tenantCode}
(ADR-0009, epic #536) tetap ada, tidak berubah — /news adalah rute
tambahan, bukan pengganti.
Juga di luar epic #536: Issue #636 (epic news_portal #631-#642/#649)
menambah validasi KONDISIONAL — hanya aktif ketika full-online R2-only
mode aktif UNTUK TENANT PEMANGGIL (isNewsPortalFullOnlineR2ModeActiveForTenant,
application/news-portal-r2-mode-gate.ts) — yang mewajibkan
featuredMediaId dan item gallery bertipe image mereferensikan baris
verified/attached di media registry news_portal (#633), bukan lagi
UUID/URL bebas TANPA verifikasi. Rule #18 di bawah ("tidak ada base
media library") TETAP BENAR untuk mode non-R2-only (mayoritas
deployment hari ini) — hanya mode R2-only yang menambah lapisan
verifikasi di atasnya, bukan mengganti aturan itu. Detail teknis lengkap
(file baru, og:image, resolusi render-time) ada di
.claude/skills/awcms-micro-news-portal/SKILL.md §636 — baca DI SANA
sebelum menyentuh featuredMediaId/gallery lagi, jangan re-derive di
sini.
Juga di luar epic #536: Issue #640 (content quality checklist preview)
menambah GET /api/v1/blog/posts/{id}/quality-checklist dan
GET /api/v1/blog/pages/{id}/quality-checklist — endpoint baca-saja,
menghitung sinyal kualitas konten (mis. panjang judul/excerpt, kelengkapan
SEO) dari data yang sudah ada, tidak menulis apa pun, tidak ada permission
baru di luar <posts|pages>.read yang sudah ada.
Juga di luar epic #536: Issue #641 (automatic internal tag linking)
menambah transform render-time yang menautkan nama tag yang cocok di body
post published ke URL arsip tag-nya — lihat domain/internal-tag-linking*.ts,
application/internal-tag-link-*.ts, migration sql/052 (tabel
awcms_micro_blog_internal_tag_link_settings, per-tenant policy), gated
deployment-wide config BLOG_AUTO_INTERNAL_TAG_LINKS_* + kolom per-post
auto_internal_tag_links_disabled (opt-out). Endpoint baru:
GET/PATCH /api/v1/blog/internal-tag-links/settings (policy tenant),
GET /api/v1/blog/posts/{id}/internal-links/preview (preview read-only,
permission preview — bukan configure, lihat aturan §Rule pada skill
awcms-micro-abac-guard untuk kenapa preview dipisah dari configure).
Admin UI: src/pages/admin/blog/internal-tag-links.astro (layar ke-15).
026_awcms_micro_blog_content_schema.sql): awcms_micro_blog_posts, _pages, _terms, _post_terms, _revisions (append-only), _redirects, _settings (1 row/tenant, tenant_id = PK). Semua ENABLE+FORCE ROW LEVEL SECURITY, tanpa GRANT eksplisit (migration 013's ALTER DEFAULT PRIVILEGES sudah meng-cover). Migration 028 (Issue #539) mengubah search_vector di posts/pages jadi GENERATED ALWAYS ... STORED (weighted title/excerpt/content_text, config simple) — PostgreSQL sendiri yang menjaganya sinkron, tidak ada trigger/application code. Migration 029 (Issue #542) menambah awcms_micro_blog_templates, _menus+_menu_items, _widgets, _ads+_ad_placements, _theme_settings (semua RLS FORCE juga) plus kolom translation_group_id di posts/pages — lihat README §Presentation extensions untuk detail tiap tabel.027, 030): 26 permission blog_content.<posts|pages|taxonomies|revisions|settings|seo|search>.<action> dari migration 027, plus 10 permission <templates|menus|widgets|ads|theme>.<read|configure> dari migration 030 (Issue #542) — kalau endpoint baru butuh permission di luar daftar ini, itu berarti scope-nya salah atau butuh migration permission baru, bukan improvisasi module_key/activity_code baru.src/modules/blog-content/domain/): content-validation.ts (field inti + penolakan HTML tak aman via containsUnsafeHtml, diekspor sejak #542 + validateDeleteReasonInput bersama, dipakai ulang oleh create/update/delete post/page/term/widget), post-status.ts (isValidStatusTransition, canRestorePost, canPurgePost), page-type.ts (isPageType), slug-policy.ts, seo-validation.ts (isAbsoluteHttpUrl, dipakai ulang untuk URL menu/ad), taxonomy-policy.ts (validateTermParent), content-access-policy.ts (evaluateContentUpdateAccess — logic ABAC ownership generik, lihat di bawah), post-access-policy.ts/page-access-policy.ts (thin wrapper resource-specific), blog-post-validation.ts, blog-page-validation.ts, blog-term-validation.ts, revision-policy.ts (isSignificantContentChange), template-policy.ts/menu-policy.ts/widget-policy.ts/ad-policy.ts/theme-policy.ts (Issue #542). Panggil fungsi-fungsi ini, jangan tulis ulang regex/aturan yang sama di endpoint handler.src/modules/blog-content/application/): blog-post-directory.ts (CRUD + lifecycle posts, plus konsumsi syncPostTermAssignments/fetchPostTermIds/countExistingTerms dari blog-taxonomy-directory.ts untuk termIds), blog-page-directory.ts (CRUD pages saja — tanpa lifecycle transition/restore/purge), blog-taxonomy-directory.ts (CRUD term + relasi post-term), blog-search.ts (searchBlogContentAdmin, searchPublicBlogContent), blog-revision-directory.ts (createBlogRevision/listBlogRevisions/fetchBlogRevisionById, INSERT-only), blog-scheduled-publish.ts (publishDueScheduledPosts, satu UPDATE set-based per tenant), template-directory.ts/menu-directory.ts/widget-directory.ts/ads-directory.ts/theme-settings-directory.ts (Issue #542, CRUD directory per resource, pola sama blog-taxonomy-directory.ts), localized-content-directory.ts (Issue #542 — setPostTranslationGroup/fetchPostTranslations, satu kolom UPDATE/SELECT yang sengaja berdiri sendiri, tidak menyentuh blog-post-directory.ts's query besar — lihat §Aturan #13).src/pages/api/v1/blog/posts/): CRUD + 5 lifecycle action + termIds/translationGroupId di body create/update + revisions sub-resource. API admin pages (src/pages/api/v1/blog/pages/): CRUD saja, tanpa lifecycle action; PATCH signifikan tetap memicu createBlogRevision (resource_type='page') walau tidak ada rute baca/restore untuk page revision. API admin terms (src/pages/api/v1/blog/terms/): list/create/update/delete, tanpa GET /{id}. API search (src/pages/api/v1/blog/search/): admin only, keyset-paginated. Baca src/modules/blog-content/README.md untuk pola guard/idempotency/audit lengkap tiap endpoint (termasuk nama action audit literal blog.<resource>.<verb>).src/pages/api/v1/blog/posts/[id]/revisions/, Issue #541): GET list, GET /{revisionId} detail (guard blog_content.revisions.read, tanpa ownership override), POST /{revisionId}/restore (guard blog_content.revisions.restore eksplisit, Idempotency-Key wajib, append-only — restore = tulis konten balik ke post + insert revisi baru, tidak pernah UPDATE/DELETE baris revisi manapun).scripts/blog-scheduled-publish.ts, Issue #541): bun run blog:publish:scheduled, worker internal (bukan endpoint HTTP) dijadwalkan cron/systemd timer, satu UPDATE idempoten per tenant untuk post status='scheduled' AND scheduled_at<=now().src/pages/blog/[tenantCode]/, Issue #540): index, detail post, arsip kategori/tag, search, feed.xml, sitemap-blog.xml — 7 APIRoute (.ts, bukan .astro, lihat §Aturan #7). Semua anonim, resolusi tenant lewat src/lib/tenant/public-tenant-resolver.ts's resolvePublicTenantByCode (ADR-0009). Query publik ada di public-blog-directory.ts (bukan blog-post-directory.ts yang admin-only), rendering aman di content-block-rendering.ts/seo-rendering.ts/public-page-rendering.ts. Hanya post, tidak ada rute publik untuk pages, widget, atau ads (helper-nya listActiveAdsForPlacement/renderAdHtml/listWidgets({activeOnly:true}) sudah ada dan teruji, belum ada rute yang memasangnya). Ini masih akurat untuk /news juga — Issue #560 (epic #555) menambah src/pages/news/ (7 APIRoute paralel: index, {slug}, category/{slug}, tag/{slug}, search, feed.xml, sitemap-news.xml) yang me-reuse persis service yang sama (public-blog-directory.ts/public-page-rendering.ts/seo-rendering.ts/content-block-rendering.ts/searchPublicBlogContent) — juga hanya post, masih tidak ada rute publik untuk pages/widget/ads di /news baik. Satu-satunya perbedaan /news dari /blog/{tenantCode}: resolusi tenant lewat resolvePublicTenantFromRequest (Issue #559, src/lib/tenant/public-host-tenant-resolver.ts) via helper withNewsTenant (application/public-news-tenant-resolution.ts), bukan resolvePublicTenantByCode dari path segment — dan /news menambahkan cek module-disabled eksplisit yang belum ada di /blog/{tenantCode} (pre-existing gap, didokumentasikan, sengaja tidak diretrofit di Issue #560 — lihat skill awcms-micro-tenant-domain-routing).src/pages/api/v1/blog/{templates,menus,widgets,ads}/ (CRUD, satu permission configure menggerbangi create/update/delete, pola sama taxonomies) dan src/pages/api/v1/blog/theme/ (GET/PATCH, satu baris per tenant, fallback ke awcms_micro_tenants.default_theme). Menu items dan ad placements full-replace via sub-array di payload (items/placements), sama seperti termIds.content-block-rendering.ts): tipe block baru { type: "gallery", items: GalleryItem[] } (image/video, whitelist render, <img>/<video controls> saja) — bukan tabel/endpoint media terpisah, karena tidak ada base media library nyata untuk diintegrasikan (featuredMediaId cuma UUID longgar tanpa FK).src/pages/api/v1/blog/settings/): GET/PATCH, satu baris per tenant (awcms_micro_blog_settings, migration 026 — kolom typed default_locale/default_visibility/posts_per_page/seo_default_title/seo_default_description ditulis/dibaca lewat route untuk pertama kali di #543; blogTitle/blogDescription/rssEnabled/sitemapEnabled di kolom jsonb settings catch-all). Directory blog-settings-directory.ts (fetchBlogSettings/upsertBlogSettings, merge-patch upsert), policy blog-settings-policy.ts (validateUpdateBlogSettingsInput). Guard blog_content.settings.{read,configure}, audit blog.settings.updated, terdaftar di OpenAPI (paths./api/v1/blog/settings) dan closing gap AsyncAPI settings.updated (lihat di bawah). feed.xml.ts/sitemap-blog.xml.ts (#540) sekarang membaca rssEnabled/sitemapEnabled dari sini — tenant yang mematikannya dapat 404 identik dengan tenant tak dikenal (sama seperti aturan #5, tidak ada sinyal pembeda).src/pages/admin/blog/): 15 layar Astro (+ vanilla JS, tanpa framework baru), reuse AdminLayout + design token existing, pola SSR-read-lewat-application-layer + mutasi-lewat-fetch-ke-endpoint-terguard identik dengan src/pages/admin/modules/[moduleKey].astro (referensi utama) — dashboard (index.astro), posts (posts/index.astro, /new, /[id] — termasuk lifecycle action + revision history), pages (pages/index.astro, /new, /[id] — tanpa lifecycle action, sesuai README §Belum tersedia), categories.astro, tags.astro (tag structural tidak punya field parentId sama sekali, bukan cuma disembunyikan), settings.astro, layar opsional templates.astro/widgets.astro/menus.astro/ads.astro (disertakan karena #542 sudah merged), dan internal-tag-links.astro (Issue #641, layar ke-15 — konfigurasi policy internal tag linking). Semua aksi high-risk (publish/schedule/archive/restore/purge/revision-restore/delete) wajib window.confirm + Idempotency-Key baru per attempt (newIdempotencyKey() di src/lib/ui/admin-form-client.ts) + tombol submit terkunci selama in-flight. module.ts's navigation array (satu entry /admin/blog, guard blog_content.posts.read) dan permissions array (39 entry, mirror migration 027+030+052) baru dideklarasikan di #543 — sebelumnya kosong meski permission-nya sudah lama ada di DB.asyncapi/awcms-micro-domain-events.asyncapi.yaml): 27 channel awcms-micro.blog-content.* (13 dari #541 + 13 dari #542 + 1 dari Issue #641 — internal-tag-linking-policy.updated), terdaftar juga di module.ts's events.publishes. Dokumentasi kontrak saja — produser nyatanya structured JSON logger (log()), bukan event bus, konvensi sama sejak Issue 0.3 (lihat pola email.* sebagai precedent). Sejak #543 semua channel punya produser nyata — settings.updated (yang sebelumnya reserved-tanpa-produser) sekarang di-log dari PATCH /api/v1/blog/settings.(tenant_id, locale, slug) selama deleted_at IS NULL; terms unik per (tenant_id, taxonomy_type, slug). Jangan tambah constraint unik baru yang lebih longgar/ketat tanpa migration baru + update README.parent_id — sudah di-enforce di constraint DB (awcms_micro_blog_terms_tag_no_parent_check) dan aplikasi (validateTermParent). Endpoint create/update term wajib panggil validateTermParent sebelum insert/update — untuk update, gabungkan field yang dikirim dengan baris existing dulu (lihat blog-term-validation.ts docblock), jangan cek isolated terhadap body saja.UPDATE/DELETE baris awcms_micro_blog_revisions. "Restore revisi" (Issue #541, POST /api/v1/blog/posts/{id}/revisions/{revisionId}/restore) = tulis konten revisi lama balik ke baris post aktif (updateBlogPost), lalu insert revisi baru lagi mencatat state hasil restore itu (changeNote: "Restored from revision {n}.") — dua langkah, keduanya lewat blog-revision-directory.ts/blog-post-directory.ts yang sudah ada, jangan tulis SQL UPDATE ... SET langsung ke tabel revisi. Revisi baru dibuat hanya kalau PATCH menyertakan title/contentJson/contentText (domain/revision-policy.ts's isSignificantContentChange) — field kosmetik (seoTitle, slug, visibility, dll.) tidak memicu revisi. Berlaku untuk posts DAN pages (dipanggil dari kedua PATCH handler), tapi rute baca/restore revisi baru ada untuk posts.search_vector sudah GENERATED ALWAYS ... STORED sejak migration 028 (Issue #539) — jangan pernah menulis ke kolom ini secara manual (Postgres menolaknya), dan jangan menambah trigger/recomputeSearchVector semacamnya, itu sudah beres di level kolom.tenant_code (/blog/{tenantCode}/...) via resolvePublicTenantByCode (src/lib/tenant/public-tenant-resolver.ts), bukan subdomain/header — base ini LAN-first, tidak boleh berasumsi ada DNS/TLS publik. tenantCode tidak ditemukan ATAU tenant tidak active → 404 yang identik (jangan bocorkan keberadaan tenant). Pakai searchPublicBlogContent (blog-search.ts, Issue #539) langsung untuk search publik, jangan tulis ulang predikat visibilitasnya.public-blog-directory.ts): LISTING (index/kategori/tag/search/feed/sitemap) pakai visibility = 'public' ketat; DETAIL (fetchPublicBlogPostBySlug) pakai visibility IN ('public', 'unlisted') — unlisted bisa diakses link langsung tapi tidak muncul di listing manapun, private tidak pernah publik sama sekali di kedua konteks. Jangan menyamakan keduanya.APIRoute .ts, bukan .astro — supaya testable lewat tests/integration/harness.ts's invoke()/invokeRaw() (pola test satu-satunya yang ada di repo ini). invokeRaw() (bukan invoke()) untuk handler yang me-return HTML/XML, bukan JSON — invoke() selalu JSON.parse dan akan throw untuk body non-JSON.content_json sekarang punya schema konkret (Issue #540, sebelumnya "opaque"): { blocks: ContentBlock[] }, 4 tipe (paragraph/heading/list/quote). Rendering SELALU lewat content-block-rendering.ts's renderContentJsonToHtml (whitelist, escape semua teks) — jangan pernah set:html/render mentah content_json/content_text di rute mana pun.blog_post_publish/_schedule/_archive/_restore/_purge (Issue #538); revisi-restore punya scope blog_revision_restore (Issue #541) — pola blog_<resource>_<action> yang sama. Pages/terms CRUD (#539), scheduled-publishing job (#541 — idempoten by construction lewat WHERE clause, bukan lewat Idempotency-Key), dan seluruh rute publik (#540, GET-only, tidak mutasi apa pun) tidak idempotency-gated — jangan tambahkan tanpa alasan baru.action memakai string literal blog.<resource>.<verb> (bukan verb generik singkat seperti modul lain) — blog.post.* (#538), blog.page.*/blog.term.* (#539), blog.post.revision_restored/blog.post.scheduled_publish_executed/blog.post.scheduled_publish_skipped (#541, reuse blog.post.published juga untuk publish via scheduled job) sudah konsisten. Rute publik (#540) tidak menulis audit event (baca-saja, anonim, tidak ada actorTenantUserId); scheduled-publishing job juga tidak set actorTenantUserId (aktor sistem/background, bukan user).content-access-policy.ts's evaluateContentUpdateAccess dipakai posts DAN pages (author boleh edit konten sendiri yang belum published, tanpa permission update). Kalau menambah resource baru dengan pola serupa, panggil fungsi generik ini dengan guard baru — jangan copy-paste logic evaluatePostUpdateAccess/evaluatePageUpdateAccess lagi. Restore revisi TIDAK punya override ini — blog_content.revisions.restore wajib eksplisit meski pemanggil adalah author post itu sendiri (beda sengaja dari update).try/catch, error asli di-log(), respons ke klien selalu string generik dari src/lib/html/error-responses.ts. Reuse fungsi itu untuk rute publik baru, jangan bikin pesan error ad-hoc.locale di posts/pages sudah cukup untuk "locale-based storage/retrieval" + slug uniqueness tenant+locale-aware (sejak #537) — jangan desain ulang jadi JSONB per-locale. Yang ditambahkan #542 hanyalah translation_group_id (nullable, tanpa FK/trigger) buat menautkan beberapa post locale-variant, lewat fungsi berdiri sendiri localized-content-directory.ts's setPostTranslationGroup — jangan tambahkan kolom ini ke INSERT/UPDATE/RETURNING di blog-post-directory.ts (file itu sudah disentuh 7+ tempat berbeda; satu fungsi sempit terpisah jauh lebih rendah risiko).blog_content, tambahkan channel+operation baru di asyncapi/awcms-micro-domain-events.asyncapi.yaml dan entry di module.ts's events.publishes (divalidasi scripts/api-spec-check.ts's checkModuleEventChannels — module.ts tanpa channel yang cocok = api:spec:check gagal). Produser event = tambahkan log("info", "blog-content.<aggregate>.<verb>", {...}) di titik kode yang sama tempat recordAuditEvent dipanggil — jangan bangun dispatcher pub/sub baru, kontrak ini dokumentasi-saja sejak Issue 0.3 (lihat precedent email.* di README email module).configure untuk create+update+delete, bukan permission per-aksi seperti posts — pola sama taxonomies.configure. Jangan tambah templates.create/templates.delete terpisah tanpa alasan baru yang kuat./news (Issue #560, epic #555) me-reuse rute publik /blog/{tenantCode} apa adanya — jangan tulis ulang query/rendering/predikat visibilitas untuk /news; satu-satunya kode baru yang boleh ditambah adalah lapisan resolusi tenant (withNewsTenant, application/public-news-tenant-resolution.ts) dan basePath link building (renderPostSummaryListHtmlAtBasePath di public-page-rendering.ts, /blog/{tenantCode}'s wrapper lama tetap byte-for-byte sama). Perubahan pada /blog/{tenantCode} existing untuk mendukung /news wajib nol-behavior-change (pure refactor/extraction saja) — lihat skill awcms-micro-tenant-domain-routing's §Rute publik /news untuk detail lengkap termasuk cek module-disabled yang /news punya tapi /blog/{tenantCode} belum.id client-supplied kalau ada hierarki/self-reference dalam satu payload (menu items' parentItemId, lihat menu-directory.ts's syncMenuItems docblock) — karena DELETE-lalu-INSERT membuang id lama sebelum baris baru ditulis, referensi ke sibling di payload yang sama HANYA bisa diselesaikan kalau klien sendiri yang menyuplai id-nya (bukan gen_random_uuid() DB). Sub-resource tanpa hierarki (ad placements) tetap boleh pakai id DB-generated biasa.content-block-rendering.ts's whitelist (pola gallery, Issue #542) atau simpan sebagai UUID/URL longgar (pola featuredMediaId), tergantung kebutuhan — jangan re-derive konsep "media library" dari nol. Sejak Issue #636 (lihat catatan "Di luar epic #536" di atas): saat full-online R2-only mode aktif untuk tenant, UUID/URL longgar itu WAJIB divalidasi menunjuk baris registry news_portal (#633) yang aman — tetap bukan media library baru di blog_content sendiri, hanya validasi referensi ke registry modul lain.awcms_micro_tenants.default_theme (migration 002) tetap satu-satunya sumber default. Tabel/endpoint theme modul manapun (blog atau modul lain di masa depan) harus fallback ke situ saat tidak ada override, sama seperti theme-settings-directory.ts's fetchBlogThemeSettings.news_portal lewat capability port, BUKAN import langsung (Issue #681, epic #679 — lihat ADR-0011 dan skill awcms-micro-news-portal's §681 untuk detail penuh). application/domain file blog_content DILARANG import ... from tree application/domain milik news_portal — kapabilitas apa pun yang dibutuhkan dari news_portal (mis. validasi/resolusi media R2) diterima lewat parameter port (_shared/ports/news-media-port.ts's NewsMediaPort), disuntikkan pemanggil (route handler). Dijaga otomatis oleh tests/unit/module-boundary.test.ts — PR yang menambah import lintas-modul baru akan gagal test ini.Epic #536 (Issue #537-#543) sudah selesai seluruhnya, tapi beberapa hal tetap di luar scope epic ini secara sengaja — jangan asumsikan sudah dikerjakan hanya karena epic-nya "selesai":
PATCH, hanya tidak ada endpoint baca/restore-nya).publish/schedule/archive/restore/purge untuk pages) — permission-nya sudah diseed sejak #537, tapi endpoint-nya tidak pernah dibangun di issue manapun (#538-#543). Jangan asumsikan itu selesai hanya karena posts sudah punya lifecycle lengkap dan admin UI post editor punya panel lifecycle.Accept-Language) — rute publik tidak memfilter berdasarkan preferensi bahasa pengunjung.version.listActiveAdsForPlacement/renderAdHtml/listWidgets({activeOnly:true})) sudah ada dan teruji sejak #542, admin CRUD-nya (/admin/blog/widgets, /admin/blog/ads) sudah ada sejak #543, tapi belum ada satu rute publik pun yang memasangnya di halaman blog.content_json di post/page editor, bukan picker visual.content_json, dan editor visual (tree/drag-drop) untuk menu items/ad placements — admin UI #543 memakai textarea JSON berlabel untuk semuanya, keputusan scope yang disengaja (lihat README §Known limitations).src/modules/blog-content/README.md §Belum tersedia berisi daftar lengkap dan detail per item.