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—
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:
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.
Metadata: Keyword-value pairs like #:title "Page Title" that define page properties.
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:
All exports from camp: get-collection, get-taxonomy-terms, get-taxonomy-pages, page-link-url, page-link-title, page-link-metas, prev, next, etc.
All exports from punct/doc: the document struct and related utilities.
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
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)
The collection-name specifies which collection to paginate. The #:per-page argument controls how many items appear on each page.
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.
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)
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?
procedure
(pagination-nav pagination [ #:always-show? always-show?]) → any/c pagination : pagination? always-show? : boolean? = #f
<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.