P-p-p-pages!

Michael on 2025-03-26

I’ve been trying to figure out how to generate pages dynamically with the Ignite static site generator, and with some help from friends, I eventually got there - here’s how!

I was trying to generate a page index for my blog. I wanted each page to have a certain number of links to posts, and there to be enough pages to cover all the posts. This had to be dynamic too, so that the pages would change as I added/removed posts from the blog, plus I also wanted to easily alter the number of posts per page and therefore the total number of pages in the index.

For example, if I have 10 posts and I wanted 5 posts per page, that would be 2 pages in total. If I added an 11th post, the site should create 3 pages. And if I decided to switch to 3 posts per page, the site would rebuild and create 4 pages.

Before I get into the details, I want to say a big “Thank you!” to:

Paul Hudson for helping me work out how to create pages dynamically (and creating Ignite too!), and to Mikaela Caron for pushing me in the right direction initially. Finally, thanks to JP Toro who fixed an Ignite issue that unlocked the final piece of the @Environment puzzle.

My solution involves a number of parts; a Constants enum, an extension to ArticleLoader, the Site, a BlogListing page, a BlogListingPage component, and a BlogListingMenu component. Note, that I am making some assumptions in this code that there will be at least 1 blog post, which meant I didn’t bother worrying about divide by zero errors.

Constants is the easiest part.

enum Constants {
	
  static var blogArticlesPerPage: Int {
    3
  }
	
}

To re-configure the page index, I need only change that 3 to another number and the correct page index and posts-per-page will be generated.

Next up is the extension for ArticleLoader, which consists of a couple of helpers to aid readability elsewhere.

extension ArticleLoader {
	
  /// Returns the number of article pages there will be
  /// e.g. if I have 10 articles in total, and Constants.blogArticlesPerPage is 5
  /// then this returns 2, i.e. 2 pages
  public var articlePageCount: Int {

    var pages = all.count / Constants.blogArticlesPerPage

    pages += all.count.isMultiple(of: Constants.blogArticlesPerPage) ? 0 : 1

    return pages
  }
	
  /// Returns an array of Article
  /// containing those Articles that will appear on the given pageNumber.
  /// e.g. Assume I have 10 articles and want 5 articles per page.
  ///      By calling the method with pageNumber = 2, it will return an array
  ///      containing the 6th, 7h, 8th, 9th, and 10th articles from the site.
  public func articlesFor(pageNumber: Int) -> [Article] {

    var articles: [Article] = []

    /// If the page is not at least 1, then we won't have any articles so return the empty array.
    guard pageNumber > 0 else {
      return articles
    }

    /// Now we must work out which articles to add to the returned array.
    /// We do this by working out a page number for each article
    /// and if it equals pageNumber, we'll add it to the returned array.
    var articleCount = 1
    var currentPageNumber = 1

    for article in all {

      /// If the current page number is the page number passed in
      /// add the article to the array of articles to return.
      if ( currentPageNumber ) == pageNumber {
        articles.append(article)
      }

      /// If we have reached the number of articles for a page
      /// we must increment the page number and reset the article count back to 0.
      /// Otherwise, increment article count again.
      if articleCount == Constants.blogArticlesPerPage {
        currentPageNumber += 1
        articleCount = 1
      } else {
        articleCount += 1
      }

    }

    return articles
  }

}

Now we move onto the BlogListingMenu component. This component can be added to any page and it will display a page index, with each page being a link to a page showing links to specific articles. I also added a link back to my Blog’s home page for ease of navigation.

struct BlogListingMenu: HTML {

  @Environment(\.articles) var articles
	
  var body: some HTML {
		
    Text {

      let separator = articles.articlePageCount > 0 ? " - " : ""
      Link("Blog", target: Blog())
      separator
			
      ForEach(1...articles.articlePageCount) { pageNumber in
      
        let separator = pageNumber < articles.articlePageCount ? " - " : ""
        Link("Page \(pageNumber)", target: "../blogpage\(pageNumber)")
        separator
        
      }
    }
  }
}

We can move onto the BlogListingPage component, which displays links to the blog posts that should appear on the given page. This uses the helper method I introduced in the ArticleLoader extension earlier.

struct BlogListingPage: HTML {
	
  let pageNumber: Int
	
  @Environment(\.articles) var articles
	
  init(pageNumber: Int) {
    self.pageNumber = pageNumber
  }
	
  var body: some HTML {
		
    ForEach(articles.articlesFor(pageNumber: pageNumber)) { article in
			
      Text {
        Link(article)
        "(article.subtitle ?? "")	"
        article.publishedDateAsString()
      }
    }
  }
}

Next up is the page that we use as the base for creating the dynamic pages, BlogListing. This page displays the page index menu from BlogListingMenu() and uses BlogListingPage() to display article links for the given pageNumber.

I gave each page a unique title by appending the pageNumber to the title. The most important part was the path value, as this is what generates the unique URL for the site. Without this alteration, when building the site Ignite would rightly complain that I have duplicate URLs. By assigning a path with `pageNumber’ appended, I was able to generate unique URLs.

BlogListing is key, because this is what is used in Site to generate as many pages as required when Ignite runs its prepare() method.

struct BlogListing: StaticPage {
	
  var title: String
  var layout: any Layout = BlogLayout()
  var path: String

  var pageNumber: Int

  init(pageNumber: Int) {
    self.pageNumber = pageNumber
    self.title = "Blog page \(pageNumber)"
    self.path = "blogpage\(pageNumber)"
  }

  var body: some HTML {

    BlogListingMenu()
      .padding(.bottom, 25)

    BlogListingPage(pageNumber: pageNumber)

  }
}

And, finally, we get to Site, where the whole thing is brought to life in the prepare() method.

mutating func prepare() async throws {

  @Environment(\.articles) var articles

  staticPages.append(Apps())
  staticPages.append(Blog())
  staticPages.append(Home())
  staticPages.append(Me())
  staticPages.append(Now())
  staticPages.append(SlashPages())
  staticPages.append(Uses())

  if articles.articlePageCount > 0 {

    for page in 1...articles.articlePageCount {
      let newPage = BlogListing(pageNumber: page)
      staticPages.append(newPage)
    }

  }

}

This method does three things; first, it reads in the site’s articles from the environment, then it adds my static pages to the staticPages array, before finishing up by adding pages dynamically to staticPages to reflect the number of article pages I need.

With this setup in place, I can now control the page index menu for my blog. By altering the value in the Constants enum, I can have the site re-configure the number of articles per page and pages in the index. If I decide the page index menu needs altering, I am able to alter BlogListingMenu to achieve that.

I’ve enjoyed re-doing my site using Ignite and solving the puzzle above. I hope it’s been useful to you!

Site generation powered by Ignite