How to not update Page-Counter on inserted blank pages with pagebreak to odd

I am currently in the process of writing my own letter-template. It should support multiple letters per file, page-numbers and double-sided printing.

I adapted How to reset the page counter after every section? - #10 by laurmaedje to reset the page-counter on each new letter.

But now the page-counter does not account for the fact, that an empty page might have been inserted if the letter was only one page long (because a pagebreak to odd has been inserted in order to support double-sided printing of the letters).

//https://forum.typst.app/t/how-to-reset-the-page-counter-after-every-section/1924/10
#let reset = <__reset>

#let subtotal() = {
  let loc = here()
  let list = query(selector(reset).after(loc))
  if list.len() > 0 {
    counter(page).at(list.first().location()).first() - 1
  } else {
    counter(page).final().first()
  }
}

#let page-numbers = context {
  let subtotal = subtotal()
  //Dont show page-numbers if there is only one page in the letter
  if subtotal == 1 {
    none
  } else {
    numbering("1 / 1", ..counter(page).get(), subtotal)
  }
}

#let letter(
  to: [],
  body,
) = {
  pagebreak(weak: true, to: "odd")
  [#metadata(none)#reset]
  counter(page).update(1)
  pagebreak(weak: true)

  set page(
    footer: context {
      align(center, page-numbers)
    },
  )

  to
  body
}

#show: letter.with(to: [To someone])

#lorem(50)

#show: letter.with(to: [To someone else])

#lorem(1000)

So my question is: How do I adapt the code, so that the subtotal-function in the MWE reports the number of pages in the letter not including the possibly inserted blank page?

Merci.

What should be the page numbering for a 3 page letter ? (EDIT: and what if the last letter has an odd number of pages? how is the last page?)
Given your code, it looks like the 4th page must be counted.
If so, you’re code must detect difference between a 1+1 empty page, and 3 + 1 empty page.
You are likely to add another label before the pagebreak(to: "odd") for this purpose (and after a pagebreak(weak: true).

In case you don’t want 4 as subtotal for a 3 pages letter, this label may be enough.

Also in the following part

You are applying the show rule to the whole document. This part is equivalent to

#letter(to: [To someone], [
  #lorem(50)
  #letter(to: [To someone else], [
    #lorem(1000)
  ])
])

Thank you.

The 3-page-letter should have the following page-numberings: 1/3, 2/3, 3/3.

In any case, only the number of actual pages in the letter should be counted, not any inserted blank pages before a new letter.

In my example, the 4th page is counted, but should not be counted (which is exactly my problem).

I try to experiment a bit further on the weekend. Perhaps i find a solution involving multiple counters.

With regard to the nested application of the show rule: What should I do instead?

with the current usage of show rule, it’s like a letter is contained inside the previous letter.
imo, it looks better to wrap each letter into a letter call

#lettter(to: [Someone])[
  First letter content
]
#lettter(to: [Someone else])[
  Second letter content
]

By doing this, the style on page is reset after the last page of each letter, therefore a the following letter introduce a blank page if the case of odd number.
Also, the letter function can now wrap the body into something else, adding an outro or padding the letter content for instance

// small size for easier testing
#set page(width: 120pt, height: 100pt)

//https://forum.typst.app/t/how-to-reset-the-page-counter-after-every-section/1924/10
#let reset = <__reset>

#let subtotal() = {
  let loc = here()
  let list = query(selector(reset).after(loc))
  if list.len() > 0 {
    counter(page).at(list.first().location()).first() - 1
  } else {
    counter(page).final().first()
  }
}

#let page-numbers = context {
  let subtotal = subtotal()
  //Dont show page-numbers if there is only one page in the letter
  if subtotal == 1 {
    none
  } else {
    numbering("1 / 1", ..counter(page).get(), subtotal)
  }
}

#let letter(
  to: [],
  from: [Anonymous],
  body,
) = {
  pagebreak(weak: true)
  [#metadata(none)#reset]
  pagebreak(weak: true, to: "odd")
  counter(page).update(1)

  set page(
    footer: context {
      align(center, page-numbers)
    },
  )

  // We can now add a now add an intro and outro
  par(strong(to))
  body
  align(right)[--- #from]
}

#letter(to: [To someone], from: [Ben])[
  #lorem(30)
]
// Letter 1
#letter(to: [To someone], lorem(10))
// Letter 2
#letter(to: [To another one], lorem(40))

#letter(to: [To the world], lorem(30))

In the case you still want to use show rules (because it’s easier too use), I think I make it work as follow (it look’s like a bit overengineered tho)

#set page(width: 120pt, height: 100pt)
#set heading(numbering: "1.1")

#let reset = <__reset>
#let last = <__last>

// Return the #last location for the current letter
#let find-last(location) = {
  let before = query(selector(last).before(location)).last(default: none)
  let reset = query(selector(reset).after(location)).first(default: none)
  let after = query(selector(last).after(location)).first(default: none)
  if reset == none {
    // last letter
    none
  } else if after == none {
    // last (empty) page of second to last letter
    before.location()
  } else if after.location().page() > reset.location().page() {
    // last (empty) page (page were #last is in)
    before.location()
  } else {
    // any other page
    after.location()
  }
}

// Global set page rule
#set page(
  footer: context {
    let last-location = find-last(here())
    let letter-page-count = if last-location != none { 
      counter(page).at(last-location).first() - 1
    } else {
      counter(page).final().first()
    }
    let current-page = counter(page).get().first()
    if letter-page-count > 1 and current-page <= letter-page-count {
      align(center, [#current-page / #letter-page-count])
    }
  },
)

#let letter-footer = {
  // Place #last label on the page following the end of the letter
  pagebreak(weak: true)
  [#metadata(none)#last]
  
  // Place #reset label on the first page of the next letter
  pagebreak(weak: true, to: "odd")
  [#metadata(none)#reset]
  counter(page).update(1)
}

#let letter(
  to: [Someone],
  body,
) = {
  // Place footer of the previous letter
  letter-footer
  
  heading(level:2)[To #to]
  
  body

  // Note, if you use show rules, putting outro there  will 
  // put all of them on the last page of the document
}

#counter(heading).step()

// Wrap single letter
#letter(text(oklch(60%,100%,  0deg,100%), lorem(10)))
#letter(text(oklch(60%,100%,100deg,100%), lorem(40)))
#letter(text(oklch(60%,100%,180deg,100%), lorem(30)))
#letter(text(oklch(60%,100%,240deg,100%), lorem(40)))

#counter(heading).step()

// Or wrap with the show rule
#show: letter

#text(oklch(60%,100%,  0deg,100%), lorem(10))

#show: letter

#text(oklch(60%,100%,100deg,100%), lorem(40))

#show: letter

#text(oklch(60%,100%,180deg,100%), lorem(30))

#show: letter

#text(oklch(60%,100%,240deg,100%), lorem(30))

Thanks a lot, this solved my question.

I opted in for the function-call and merged it with the other part.

The final code now looks like this (of course more styling will be applied :smile:)

//https://forum.typst.app/t/how-to-reset-the-page-counter-after-every-section/1924/10
//and https://forum.typst.app/t/how-to-not-update-page-counter-on-inserted-blank-pages-with-pagebreak-to-odd/8233/4
#let reset = <__reset>
#let last = <__last>

// Return the #last location for the current letter
#let find-last(location) = {
  let before = query(selector(last).before(location)).last(default: none)
  let reset = query(selector(reset).after(location)).first(default: none)
  let after = query(selector(last).after(location)).first(default: none)
  if reset == none {
    // last letter
    none
  } else if after == none {
    // last (empty) page of second to last letter
    before.location()
  } else if after.location().page() > reset.location().page() {
    // last (empty) page (page were #last is in)
    before.location()
  } else {
    // any other page
    after.location()
  }
}

#let letter-footer = {
  // Place #last label on the page following the end of the letter
  pagebreak(weak: true)
  [#metadata(none)#last]

  // Place #reset label on the first page of the next letter
  pagebreak(weak: true, to: "odd")
  [#metadata(none)#reset]
  counter(page).update(1)
}

#let letter(
  to: [],
  opening: [],
  closing: [#v(0.5cm) Greetings #v(1cm) #line(length: 2.5cm) Me],
  header: [#align(right)[*My Header*]],
  footer: [Legal Stuff],
  body,
) = {
  letter-footer
  set page(
    footer: context {
      let last-location = find-last(here())
      let letter-page-count = if last-location != none {
        counter(page).at(last-location).first() - 1
      } else {
        counter(page).final().first()
      }
      let current-page = counter(page).get().first()
      if letter-page-count > 1 and current-page <= letter-page-count {
        align(center, [#current-page / #letter-page-count])
      }
      if current-page == 1 {
        footer
      }
    },
  )

  to
  v(3cm)
  opening
  parbreak()
  body
  parbreak()
  closing
}

#letter(to: [To \ Someone], opening: [Dear someone])[
  #lorem(1000)
]

#letter(to: [To \ Someone Else], opening: [Dear someone else])[
  #lorem(1500)
]

#letter(to: [The \ Third], opening: [Dear third])[
  #lorem(50)
]