On this page:
3.1 Clear a spot to work in
3.2 Installing as a Racket package
3.3 Configure your site
3.4 First post
3.5 Add a render function
3.6 Adding Static Assets
3.7 Building Your Site
3.8 Add a home page
3.9 Add a meta-language
3.10 Add a feed
3.11 What’s next
9.1

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.