3 Building a Camp Site
Here I’ll explain how to build a website in Camp from scratch. As explained in Quick Start, you can use raco camp commands to jump-start much of this, but a no-frills explanation is good for you and builds character.
3.1 Clear a spot to work in
Start by creating a directory for your site. We’ll call it "myblog":
mkdir myblog |
cd myblog |
Create a directory for your posts and another for your static assets:
mkdir posts |
mkdir static |
Now you have your folder structure.
3.2 Installing as a Racket package
This part isn’t strictly necessary. But Camp makes use of conveniences provided by the Racket package system, so it’s simpler just to do it now.
A Camp site is a pile of code that produces a bunch of web pages. You can (and should) install that pile of code as a Racket package.
Create "info.rkt" with the following content:
"info.rkt"
#lang info (define collection "myblog") (define deps '("base" "camp")) (define camp-site "site.rkt")
The collection line gives your site a name that Racket can use to find its modules. The deps line declares that your site depends on Camp (and "base", which is Racket’s core library). The camp-site line tells Camp which file contains your site configuration.
Now install your site as a local package so Racket can find it:
raco pkg install |
Run this command from inside your "myblog" directory. You only need to do this once.
3.3 Configure your site
Create a new file in your project’s root folder:
"site.rkt"
#lang camp/site title = "My Blog" url = "https://example.com" founded = 2026-01-01 authors = ["Your Name (you@example.com)"] [[collections]] name = "posts" source = "posts/*" output-paths = "posts/[yyyy]/[MM]/*/" render-with = "(myblog/render render-post)" order = "descending" sort-key = "date"
The #lang camp/site language uses TOML syntax.
Camp always looks for a "site.rkt" module before it does anything. So now Camp knows how your site is organized and how to publish it. We just need to add the stuff that your "site.rkt" refers to.
3.4 First post
Your "site.rkt" specified a single collection named "posts", whose source files are located in the "posts/" subfolder. So let’s put a post in that folder:
The Punct language is essentially a Markdown environment which allows escaping to Racket with the • character, and which compiles to a format-independent AST. Read Writing Punct for more about its syntax.
"posts/hello-world.md.rkt"
#lang punct --- title: Hello, World date: 2026-01-15 --- # Welcome This is my first blog post. I'm building a site with *Camp*, a static site generator for Racket. Here's something Markdown can't normally do: today's year is •(date-year (seconds->date (* 0.001 (current-inexact-milliseconds)))).
3.5 Add a render function
In the [[collections]] section, your "site.rkt" included a render-with directive. This points Camp to the module, and the function within that module, that must be used to render source documents in that collection to HTML.
See Module Paths in the Racket Guide
In your case, you told Camp the render function for the "posts" collection would be the render-post function provided by the myblog/render module. So let’s create that file. The site is already installed as a Racket package that uses the myblog collection name, so a "render.rkt" located in our root folder will answer to the myblog/render module path:
"render.rkt"
#lang racket/base (require camp punct/fetch) (provide render-post) (define (render-post doc ctxt) (define title (get-meta doc 'title "Untitled")) `(html (head (meta ((charset "utf-8"))) (link ((rel "stylesheet") (href "/style.css"))) (title ,title)) (body (header (h1 ,title)) (article ,@(camp-doc->html-xexpr doc)) (footer (p "Powered by Camp")))))
Your render function must take a Punct document and a render context, and return an x-expression representing the complete HTML page.
3.6 Adding Static Assets
Most sites need stylesheets, images, or JavaScript files. Camp copies everything in your static folder to the output directory unchanged. Create a basic stylesheet in your "static/" subfolder:
"static/style.css"
body {
max-width: 40rem;
margin: 2rem auto;
padding: 0 1rem;
font-family: system-ui, sans-serif;
line-height: 1.5;
}
header { margin-bottom: 2rem; }
footer { margin-top: 3rem; color: #666; }
3.7 Building Your Site
With all of this in place, you can now build your site:
raco camp build |
● Collect 1 page in 1 collection 63ms |
● Build 1 page 20ms |
● Static 1 file |
|
✓ Done in 149ms |
You’ll see the site’s files and folders in a new "publish/" subfolder.
To preview the site:
raco camp serve |
Serving /Users/joel/code/myblog/publish |
URL http://localhost:8000 |
Watching for changes... |
Press Ctrl+C to stop |
Browse to http://localhost:8000/posts/2026/01/hello-world/ to see the post we created.
3.8 Add a home page
Add a second collection to "site.rkt":
"site.rkt"
#lang camp/site title = "My Blog" url = "https://example.com" founded = 2026-01-01 authors = ["Your Name (you@example.com)"] [[collections]] name = "posts" source = "posts/*" output-paths = "posts/[yyyy]/[MM]/*/" render-with = "(myblog/render render-post)" order = "descending" sort-key = "date" [[collections]] name = "pages" source = "pages/*" output-paths = "*/" render-with = "(myblog/render render-page)"
Create a "pages/" subfolder and an index page:
"pages/index.md.rkt"
#lang punct --- title: Home output-path: / --- # Welcome to My Blog
The output-path metadata overrides the collection’s default output path for this page.
Add render-page to "render.rkt":
"render.rkt"
#lang racket/base (require camp punct/fetch racket/list) (provide render-post render-page) (define (render-post doc ctxt) (define title (get-meta doc 'title "Untitled")) `(html (head (meta ((charset "utf-8"))) (link ((rel "stylesheet") (href "/style.css"))) (title ,title)) (body (header (nav (a ((href "/")) "Home")) (h1 ,title)) (article ,@(camp-doc->html-xexpr doc)) (footer (p "Powered by Camp"))))) (define (render-page doc ctxt) (define title (get-meta doc 'title "Untitled")) (define posts (get-collection "posts" #:limit 5)) `(html (head (meta ((charset "utf-8"))) (link ((rel "stylesheet") (href "/style.css"))) (title ,title)) (body (header (h1 ,title)) (main ,@(camp-doc->html-xexpr doc) (h2 "Recent Posts") (ul ,@(for/list ([p posts]) `(li (a ((href ,(page-link-url p))) ,(page-link-title p)))))) (footer (p "Powered by Camp")))))
3.9 Add a meta-language
You can make Camp’s functions available inside source documents by creating a "main.rkt" that Punct can use as a meta-language:
"main.rkt"
#lang racket/base (require camp camp/xref) (provide (all-from-out camp) (all-from-out camp/xref))
Now source files that use #lang punct myblog can call get-collection, ~d, defterm, etc. directly.
3.10 Add a feed
Append a [[feeds]] section to "site.rkt":
"site.rkt (append)"
[[feeds]] filename = "feed.atom" collections = ["posts"] render-with = "(myblog/render feed-content)"
The extension determines the format: ".atom" → Atom, ".rss" → RSS.
Add the feed renderer to "render.rkt":
"render.rkt (addition)"
(provide render-post render-page feed-content) (define (feed-content doc) (camp-doc->html-xexpr doc))
Posts missing a date or marked draft: true are excluded from feeds.
3.11 What’s next
Tutorial: Navigation and Cross-References covers Camp’s cross-reference system. Tutorial: Listing and Index Pages introduces a language for aggregate pages like archives and tag indices.