On this page:
8.4.1 Document Structure
8.4.2 Available Bindings
8.4.3 Render Function Integration
camp-page-doc?
8.4.4 Pagination
paginate
pagination
paginated-content
pagination-nav
9.1

8.4 Structural Page Language🔗ℹ

 #lang camp/page package: camp-lib

The #lang camp/page language provides an alternative to #lang punct for pages that are primarily structural or organizational—such as tag listings, index pages, and archives—where CommonMark processing is not needed and full Racket control over the output is desired.

Unlike Punct documents, #lang camp/page documents do not pass through a CommonMark parser. Instead, body expressions are wrapped in a thunk and evaluated at render time when site information is available. This allows direct use of get-collection, get-taxonomy-terms, get-taxonomy-pages, and other retrieval functions within the page content.

8.4.1 Document Structure🔗ℹ

A #lang camp/page document consists of three sections:

  1. Module-level forms (optional): require, provide, and define forms that appear before any metadata. These are lifted to module level and evaluated at load time.

  2. Metadata: Keyword-value pairs like #:title "Page Title" that define page properties.

  3. Body: All remaining forms, including any define forms after metadata. These are wrapped in a thunk and evaluated at render time when current-site-info is available.

#lang camp/page
 
(require racket/string)  ; lifted to module level
 
;; Helper defined before metadata - lifted to module level
(define (format-tag tag)
  (string-titlecase tag))
 
#:title "Browse by Tag"
#:slug "tags"
 
;; Everything after metadata is in the body thunk
(define tag-data (get-taxonomy-pages "blog" "tags"))
 
`(div ((class "content"))
   (h1 "Browse by Tag")
   (p "Posts organized by topic:")
   ,@(for/list ([tag (get-taxonomy-terms "blog" "tags")])
       `(section ((class "tag-section"))
          (h2 ,(format-tag tag))
          (ul ,@(for/list ([pg (hash-ref tag-data tag)])
                  `(li (a ((href ,(page-link-url pg)))
                          ,(page-link-title pg))))))))

Metadata is specified using keyword-value pairs. The value following each keyword is read as a Racket datum.

Keyword

  

Description

#:title

  

Page title (used in templates and page index)

#:slug

  

URL slug (defaults to filename if not specified)

#:date

  

Publication date (for sorting and feed inclusion)

#:draft?

  

If @racket[#t], excludes from feeds

#:output-path

  

Override the collection's output path pattern

Any other keywords are stored in the document metadata and accessible via meta-ref.

8.4.2 Available Bindings🔗ℹ

The #lang camp/page language provides all bindings from racket/base, plus:

Additional modules can be required as needed.

8.4.3 Render Function Integration🔗ℹ

Documents written in #lang camp/page produce a Punct-compatible doc binding with the body thunk stored in metadata. When render functions call camp-doc->html-xexpr, it detects #lang camp/page documents and evaluates the thunk to produce the body content.

procedure

(camp-page-doc? doc)  boolean?

  doc : any/c
Returns #t if doc is a document produced by #lang camp/page, #f otherwise. Use this predicate to handle #lang camp/page documents differently from Punct documents in render functions.

Render functions call camp-doc->html-xexpr to convert the document. For #lang camp/page documents, this calls the body thunk; for Punct documents, it renders the Punct content:

(require camp/page)  ; for camp-page-doc?
 
(define (render-page doc ctxt)
  (define body (camp-doc->html-xexpr doc))
 
  (if (camp-page-doc? doc)
      ;; camp/page: body already includes all markup
      (layout (meta-ref doc 'title) `((article ,@body)))
      ;; punct: add title heading
      (layout (meta-ref doc 'title)
              `((article (h1 ,(meta-ref doc 'title)) ,@body)))))

8.4.4 Pagination🔗ℹ

Camp provides pagination support for creating classical blog-style index pages that display multiple posts per page with “Older/Newer” navigation. Pagination is implemented using the paginate form within #lang camp/page documents.

procedure

(paginate collection-name    
  #:per-page per-page    
  [#:page-slug page-slug]    
  render-proc)  paginated-content?
  collection-name : string?
  per-page : exact-positive-integer?
  page-slug : string? = "page"
  render-proc : (-> (listof page-link?) pagination? any/c)
Creates paginated output from a collection. When the build system encounters a paginated-content value in a #lang camp/page body, it generates multiple output files—one for each page of results.

The collection-name specifies which collection to paginate. The #:per-page argument controls how many items appear on each page.

The #:page-slug argument controls the URL structure for pages 2 and beyond. With the default "page", a page with #:output-path "/blog/" produces:
  • Page 1: "/blog/"

  • Page 2: "/blog/page/2/"

  • Page 3: "/blog/page/3/"

With #:page-slug "p", the URLs become "/blog/p/2/", "/blog/p/3/", etc.

The render-proc is called once for each generated page. It receives two arguments:
  • items: A (listof page-link?) containing the items for the current page

  • pagination: A pagination struct with navigation information

The procedure should return an x-expression for the page body.

#lang camp/page
 
#:title "Blog"
#:output-path "/blog/"
 
(paginate "blog" #:per-page 10
  (λ (items pagination)
    `(main
      (h1 "Blog")
      ,@(for/list ([p items])
          `(article
            (h2 (a ([href ,(page-link-url p)]) ,(page-link-title p)))
            ,(camp-doc->html-xexpr (page-link-doc p))))
      ,(pagination-nav pagination))))

Page titles are automatically modified for pages 2+: if the original title is “Blog”, page 2 becomes “Blog - Page 2”, page 3 becomes “Blog - Page 3”, etc.

If the collection is empty, one page is still generated with an empty items list.

struct

(struct pagination (page-num
    total-pages
    total-items
    base-url
    current-url
    prev-url
    next-url)
    #:transparent)
  page-num : exact-positive-integer?
  total-pages : exact-positive-integer?
  total-items : natural?
  base-url : string?
  current-url : string?
  prev-url : (or/c string? #f)
  next-url : (or/c string? #f)
A struct containing pagination context, passed to the render procedure in paginate.

  • page-num: The current page number (1-indexed)

  • total-pages: Total number of pages

  • total-items: Total number of items in the collection

  • base-url: URL of page 1 (e.g., "/blog/")

  • current-url: URL of the current page

  • prev-url: URL of the previous page, or #f on page 1

  • next-url: URL of the next page, or #f on the last page

struct

(struct paginated-content (collection-name
    per-page
    page-slug
    render-proc)
    #:transparent)
  collection-name : string?
  per-page : exact-positive-integer?
  page-slug : string?
  render-proc : procedure?
Internal struct returned by paginate. The build system detects this value and generates multiple output files accordingly. You should not need to create this struct directly.

procedure

(pagination-nav pagination    
  [#:always-show? always-show?])  any/c
  pagination : pagination?
  always-show? : boolean? = #f
Generates a navigation x-expression for pagination. Returns a <nav> element with previous/next links and a page indicator:

<nav class="pagination">

  <a href="/blog/" class="pagination-prev">← Newer</a>

  <span class="pagination-info">Page 2 of 5</span>

  <a href="/blog/page/3/" class="pagination-next">Older →</a>

</nav>

By default, returns '() (empty list) when there is only one page, since navigation is not needed. Pass #:always-show? #t to display the navigation even for single-page results.

The generated markup uses CSS classes for styling: pagination, pagination-prev, pagination-info, and pagination-next.