@@ -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