Why does my first page footer also appear on odd pages?

I’m designing a document with multiple “chapters”; specifically, it’s a journal. Each chapter is a different article with a different author and so on. I’m using H1 as the chapter / article title.

Each article section (as well as the foreword) should have a specific footer on the first page, and then the rest of the pages have a footer that alternates whether it’s a recto or verso page.

The relevant footer code is the following:

// footer of first page of each "section" has journal metadata
#let section_firstpage_footer = context [
  #meta.journal, #meta.volume, #meta.date.display("[month repr:long] [year]")
  #h(1fr)
  #counter(page).display()
]

// footer of rest of article has article metadata
#let article_body_footer(article) = context {
  if calc.even(counter(page).get().first()) [
    // if verso, numbers outside and title inside
    #counter(page).display()
    #h(1fr)
    #if article.keys().contains("short_title") [
      #article.short_title
    ] else [
      #article.title.split(":").first()
    ]
  ] else [
    // if recto, author inside and numbers outside
    #text(style: "italic", article.author)
    #h(1fr)
    #counter(page).display()
  ]
}

// journal metadata on first page of section, article metadata on rest
#let article_footer(article) = context {
  // if page has h1, it means it's the first page of a section
  let is_section_first_page = query(heading.where(level: 1)).any(m =>
    counter(page).at(m.location()) == counter(page).get()
  )
  if is_section_first_page {
    section_firstpage_footer
  } else {
    article_body_footer(article)
  }
}

And it’s used like so:

// foreword
#pagebreak(weak: true, to: "odd")
#page(
  footer: article_footer(meta.foreword),
  [ // ...
  ]
)

But on the recto pages of the article body, it shows the first page footer:

Except it works for the second article:

I made an example document in the web-app to share.

I started with Typst yesterday so I think I may be missing some things. Is there something wrong with doing this?

#let is_section_first_page = query(heading.where(level: 1)).any(m =>
  counter(page).at(m.location()) == counter(page).get()
)

Or is it something about context that I’m missing? I suspect it’s that.

If anyone has any general comments about the best way to structure a multi-section document or something else about how I’ve done things, please feel free to let me know as well (e.g., my next mission is to include the author names in the table of contents).

I feel like I’m missing an abstraction like chapter and part in Latex, or section as in MS Word, or even something as abstract as a div which I can target with CSS. I’ve been using #page to encapsulate sections (since they conveniently automatically overflow onto multiple pages), but in the article sections I need to start a new page to have the two-column layout (or should I manually do a page break and call #columns inside the page content? — Edit: I did this, for conistency), so I’m also relying H1s to indicate the start of a section, but then I have to strip off the first part of the section numbering so it doesn’t include a chapter number that I don’t use and that I have to manually increment as well… It feels like I’m doing something wrong. What’s the “native” way that Typst understands sections? Is it basically just following H1s?

The problem is definitely in this code, it’s the use of counter(page) instead of location.page(). Since you have front, main and backmatter, and reset the page counter, there are level 1 headings on pages with these numbers: page iii has the table of contents, page v has the foreword, pages 3 and 5 are treated as starting an article. The location instead tracks the actual physical page instead of the rendered numbers.

So use m.location().page()/here().page() instead of counter(page).at(m.location())/counter(page).get().

I would recommend this formulation of that code instead:

  let this_articles_heading = query(heading.where(level: 1).before(here())).last()
  let is_section_first_page = this_articles_heading.location().page() == here().page()

since even the first footer comes after the page, before(here()) is harmless here, and means that you only have to check the single preceding heading, instead of trying all. But the check is exactly what you had in your call to any().

2 Likes

Oh, I didn’t realise counter(page).at(...) gives the value that’s been reset. It makes sense now, but I got that examples from the docs (can’t find it now though), and I didn’t realise that that docs example assumed that one doesn’t reset the counter.

Your solution worked, thank you very much.

Regarding the other remarks: you code is a bit too long for me to analyze it fully, but I can say this much:

There is no “part” abstraction with Typst, and level 1 headings are indeed how you would structure the articles. I see that you manually start a new page for each article; an easier way to do this is to make a show rule for level 1 headings: typst-diploma-thesis/src/structure.typ at main · TGM-HIT/typst-diploma-thesis · GitHub (it’s a thesis with chapters instead of a journal with articles, but maybe this helps for inspiration).

It’s possible using page as an element, but imo it makes more sense (and will be more familiar to other Typst users) to only do that when that page doesn’t overflow and to use set page() otherwise. Using set page() will also start a new page (unless it’s literally set page() and doesn’t actually change the page setup. If you just want scoping, you can write #[...], no need to make it #page[...]. And of course using functions and modules for structuring is a good idea.

For multicolumn content that starts after a single-column title, you can use page(columns: ...), but put the title in a float. The docs have an example: Advanced Styling – Typst Documentation. Using page(columns: ...) instead of the columns(...) element is generally preferable; iirc because the latter is a block-level element and thus can’t contain pagebreaks or something.

That is a bit more than I can gleam quickly from your code; maybe you can ask that as a separate question with a more minified example for that purpose?

Welcome to the Typst community :slight_smile:

1 Like

Thanks for the advice and the welcome.

I’ve tried automatically adding a page break before H1, and setting the changes as they come, as opposed to having them attached to an environment. I don’t like this approach, to be honest, I prefer a declarative over an imperative style.

For example, with this:

// table of contents
#[
  #set page(footer: plain_footer)
  #outline(depth: 1)
]

// section
#[
  #set page(footer: plain_footer)
  #heading(level: 1, [Heading])
  // content
]

the inserted blank page now has an unwanted footer, which it didn’t have previously. It seems to me that if I don’t use #page (to which I can attach both settings and content), the settings “overflow” / escape their context in ways that don’t make sense to me.

It makes more sense to me to have the settings (explicitly) bound to a context; I think “this section has these properties” (declarative) as opposed to “now make a new page, now make the footer like this, now do this, now do that” (imperative).

Besides, aren’t these

#page(arguments, [body])

#page(arguments)[body]

#page(arguments)
body

all equivalent? I got the idea somewhere that it’s just syntactic sugar.

Assuming the last should say set page(arguments), yes.

Interesting, I would have viewed it the other way round. I guess viewing function calls similar to HTML, declaratively describing the hierarchical structure of the document, makes sense as well.

I’m not suggesting that you remove structure from your page settings and apply them ad hoc; just using different syntactic sugar with the same structure, based on me finding it more intuitive to only use page elements when they represent single pages, like the title page or colophon.

In any case, this is just about code style; both are ultimately equivalent. I want to bring one more thing up though (and both styles work with this too): your example does only a page setting. Imagine you had a section that required a different text color (or whatever):

// section
#[
  #set page(footer: plain_footer)
  #set text(red)

  #heading(level: 1, [Heading])
  // content
]

To abstract this, you could pull out a template function:

#let section(body) = {
  set page(footer: plain_footer)
  set text(red)
  body
}
// section
#[
  #show: section

  #heading(level: 1, [Heading])
  // content
]

Or equivalently using (I assume) your preferred style:

#section[
  #heading(level: 1, [Heading])
  // content
]

(I think the implementation of section looks more uniform with the set page style, but it’s also easy to change of course.)


I’m glad you brought that up, skipping headers and footers on empty pages is a problem with Typst and you got me to think about this problem in a new way.

Just to make this example a bit more self-contained:

Reference example
#show heading.where(level: 1): it => {
  pagebreak(to: "odd", weak: true)
  it
}

#[
  #set page(footer: [...])
  #lorem(30)
]

#[
  #set page(footer: [...])
  = Heading
  #lorem(30)
]

The problem is that the pagebreak is on a page with header and footer, so your original approach, with the pagebreak in between, worked:

#[
  #set page(footer: [...])
  #lorem(30)
]

#pagebreak(to: "odd", weak: true)

#[
  #set page(footer: [...])
  = Heading
  #lorem(30)
]

But, and this is a new realization, we can set page properties on the page break:

#show pagebreak.where(to: "odd"): set page(header: none, footer: none)
#show heading.where(level: 1): it => {
  pagebreak(to: "odd", weak: true)
  it
}

#set page(footer: [...])
#lorem(30)

= Heading
#lorem(30)

This is a bit of change in the hierarchy; your original example showed this for outline and section, but let’s say we have two sections instead, i.e. both pages logically have the same styles anyway.

Before:

  • all pages share general page settings (size, margins)
  • the first section has a footer
  • before the second section comes a page break/page skip. since it’s not part of a section, it doesn’t have a footer
  • the second section has a footer; indeed the same footer

After:

  • all pages share general page settings (size, margins)
  • all sections have a footer
  • before each section comes a page break/page skip. Page skips don’t have a header or footer

At least to me, that’s quite elegant :slight_smile:

3 Likes