Skip to content

Commit 446166f

Browse files
daffaromeroclaude
andcommitted
fix: filter empty structural nodes (phantom BAB sections)
Laws like UU 6/2023 (Ciptaker) wrap a full law as LAMPIRAN. The parser picks up the LAMPIRAN's table of contents as real BAB nodes, producing heading-only sections with no Pasal content in the reader. Add a lightweight parallel query fetching all parent_id values for pasals of the current work. Build structuralIdsWithPasals Set. Filter babNodes so only top-level structural nodes (BAB/aturan/lampiran) with at least one pasal directly or via a direct child section are rendered. Sub-sections (Bagian/Paragraf, parent_id != null) are kept unconditionally. Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0939875 commit 446166f

File tree

1 file changed

+40
-4
lines changed
  • apps/web/src/app/[locale]/peraturan/[type]/[slug]

1 file changed

+40
-4
lines changed

apps/web/src/app/[locale]/peraturan/[type]/[slug]/page.tsx

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ async function LawReaderSection({
226226
const supabase = await createClient();
227227

228228
// Fire all initial queries in parallel (1 RTT instead of 2)
229-
const [{ count: totalPasalCount }, { data: structure }, { data: initialPasals }, { data: rels }] = await Promise.all([
229+
const [{ count: totalPasalCount }, { data: structure }, { data: initialPasals }, { data: rels }, { data: pasalParentIds }] = await Promise.all([
230230
supabase
231231
.from("document_nodes")
232232
.select("id", { count: "exact", head: true })
@@ -250,6 +250,15 @@ async function LawReaderSection({
250250
.select("*, relationship_types(code, name_id, name_en)")
251251
.or(`source_work_id.eq.${workId},target_work_id.eq.${workId}`)
252252
.order("id"),
253+
// Lightweight query: just parent_id values for all pasals, used to filter
254+
// structural nodes (BABs, Bagians) that have no pasal content — these are
255+
// typically table-of-contents entries parsed as structural nodes.
256+
supabase
257+
.from("document_nodes")
258+
.select("parent_id")
259+
.eq("work_id", workId)
260+
.eq("node_type", "pasal")
261+
.not("parent_id", "is", null),
253262
]);
254263

255264
// Structured laws must always load all pasals SSR so the BAB-grouping logic has the
@@ -292,19 +301,46 @@ async function LawReaderSection({
292301

293302
const pageUrl = `https://pasal.id/peraturan/${type.toLowerCase()}/${slug}`;
294303

304+
// Build a set of structural node IDs that have at least one pasal child.
305+
// Used to skip empty structural nodes (e.g. TOC entries parsed as BAB nodes).
306+
const structuralIdsWithPasals = new Set(
307+
(pasalParentIds || []).map((r) => r.parent_id).filter(Boolean),
308+
);
309+
295310
// Build tree structure
296-
const babNodes = structuralNodes || [];
311+
const allStructuralNodes = structuralNodes || [];
297312
const allPasals = pasalNodes || [];
298313

314+
// Filter out structural nodes (BABs, Bagians) that have no pasal content in the DB.
315+
// This removes table-of-contents entries that the parser mistakenly captures as structural
316+
// nodes — common in ratification laws (e.g. UU 6/2023) where the attached law's TOC
317+
// appears verbatim and gets parsed as BAB markers without any associated Pasal content.
318+
// Only top-level structural nodes (BAB / aturan / lampiran — those without a parent) are
319+
// filtered; sub-sections (Bagian, Paragraf) are kept as-is under their parent BAB.
320+
const babNodes = allStructuralNodes.filter((node) => {
321+
// Keep sub-sections unconditionally — they're filtered indirectly via their parent BAB.
322+
if (node.parent_id !== null) return true;
323+
// For top-level structural nodes, keep only those that have at least one pasal
324+
// directly or through any of their direct children (Bagian/Paragraf).
325+
const childIds = new Set(
326+
allStructuralNodes.filter((n) => n.parent_id === node.id).map((n) => n.id),
327+
);
328+
return (
329+
structuralIdsWithPasals.has(node.id) ||
330+
[...childIds].some((id) => structuralIdsWithPasals.has(id))
331+
);
332+
});
333+
299334
const mainContent = (
300335
<>
301336
{babNodes.length > 0 ? (
302337
babNodes.map((bab) => {
303-
// Filter pasals for this BAB
304-
const directPasals = allPasals.filter((p) => p.parent_id === bab.id);
338+
// Find direct sub-sections (Bagian/Paragraf) of this BAB
305339
const subSectionIds = new Set(
306340
babNodes.filter((n) => n.parent_id === bab.id).map((n) => n.id),
307341
);
342+
// Filter pasals for this BAB
343+
const directPasals = allPasals.filter((p) => p.parent_id === bab.id);
308344
const nestedPasals = allPasals.filter(
309345
(p) => subSectionIds.has(p.parent_id ?? -1),
310346
);

0 commit comments

Comments
 (0)