This guide provides a deep dive into Eden's architecture, implementation details, and extension points for developers who want to understand or extend the system.
Eden follows a pipeline architecture where data flows through distinct phases:
Configuration → Loading → Processing → Rendering → Output
↓ ↓ ↓ ↓ ↓
site.edn Content Templates HTML dist/
+ Assets + Directives Generation files
eden.core- Entry points and orchestrationeden.config- Configuration parsing and URL strategieseden.loader- Content and template loadingeden.site-generator- Template directive processingeden.renderer- HTML generation and page assemblyeden.builder- File output and asset copyingeden.pipeline- Build orchestrationeden.mcp- MCP server with clojure-mcp integration
The build starts by loading site.edn and parsing configuration:
{:site-config config
:site-dir (parent-of-site-edn)
:output-dir "dist"
:mode :dev/:prod
:verbose? true/false}URL strategies are compiled into functions:
:flat→page.html:nested→page/index.html- Custom functions for advanced routing
- Scans
content/<lang>/directories - Supports
.ednand.md(with frontmatter) - Markdown is converted to HTML immediately
- Content is indexed by key derived from file path:
content/en/home.edn→:homecontent/en/blog/post-1.md→:blog.post-1- Directories become namespace separators with dots
{:en {:home {:title "Welcome"
:content/html "<p>...</p>"
:template :home}
:about {:title "About"
:slug "about"
:template :page}}
:no {:home {:title "Velkommen"
:content/html "<p>...</p>"}}}- Loads
.ednfiles fromtemplates/ - Templates are Hiccup data structures
- Supports SCI evaluation for dynamic templates
Templates are first expanded to identify dependencies:
;; Before expansion
[:eden/link :about [:a {:href [:eden/get :link/href]} "About"]]
;; After expansion
{:eden/expanded :link
:expanded-arg {:content-key :about}
:body [[:a {:href [:eden/get :link/href]} "About"]]
:eden/references #{:about}}Directives are processed via multimethods based on element type. The dispatch determines whether an element is:
- An Eden directive (
:eden/get,:eden/link, etc.) - A Hiccup element (regular HTML tags)
- A scalar value (strings, numbers, etc.)
- Other special cases
All directives:
:eden/get- Retrieve values from context data:eden/get-in- Nested data access with paths:eden/each- Iterate over collections:eden/if- Conditional rendering:eden/link- Smart page linking with dependency tracking:eden/render- Render nested components:eden/t- Translation with interpolation:eden/with- Merge data into context:eden/body- Insert page body content:eden/include- Include other templates
The rendering phase follows this flow:
- Start with render-roots: Begin with pages specified in
:render-roots - Expand templates: Process each template to find
:eden/linkdirectives - Collect dependencies: Build a graph of which pages link to which
- Compute transitive closure: Find all pages reachable from roots
- Render each page: Process discovered pages through their templates
For each page to be rendered:
- Load page content: Get the content file (e.g.,
home.edn) - Identify template: Use
:templatefield from content, or default to content key - Build page context: Merge page content into
:datafield - Process template: Expand all directives with page data
- Wrap in layout: Insert result into wrapper template's
:eden/body - Generate HTML: Convert Hiccup to HTML string
;; Simplified page rendering flow
(defn render-page [context page]
(let [;; Page content becomes :data
page-content (:content page)
template-key (or (:template page-content)
(:content-key page))
template (get templates template-key)
;; Process template with page data
page-context (assoc context :data page-content)
page-html (process template page-context)
;; Wrap in site layout
wrapper (:wrapper config)
wrapper-context (assoc context :body page-html)
final-html (process wrapper wrapper-context)]
(str "<!DOCTYPE html>"
(replicant/render final-html))))Given this setup:
;; site.edn
{:render-roots #{:home}
:wrapper :base}
;; content/en/home.edn
{:title "Welcome"
:template :home-page
:featured-product :products.widget}
;; templates/home-page.edn
[:div
[:h1 [:eden/get :title]]
[:eden/link :about
[:a {:href [:eden/get :link/href]} "About Us"]]
[:eden/render :featured-product]]Processing steps:
- Start with
:home(from render-roots) - Expand template, find link to
:about - Add
:aboutto pages to render - Render
:homewith its data (:title= "Welcome") - Render
:aboutwith its data - Both wrapped in
:basetemplate
During rendering, each directive has access to:
{:data {...} ; Current page/component data
:body [...] ; Body content for wrapper
:lang :en ; Current language
:path [:products :x] ; Page hierarchy path
:pages {...} ; All pages registry
:sections {...} ; Section registry
:strings {...} ; Translations
:content-data {...} ; All content for language
:templates {...} ; All templates
:page->url fn ; URL generation function
:site-config {...} ; Site configuration
:build-constants {...}; Build-time data
:warn! fn} ; Warning collectorFiles are written to disk with the configured URL strategy:
- Pages are written as HTML files
- Assets are processed and copied
- Build reports are generated
Eden uses esbuild for CSS and JavaScript processing when available:
CSS Processing:
- Bundles CSS files with dependencies
- Minifies in production mode
- Falls back to simple copy if esbuild not installed
JavaScript Processing:
- Bundles JS modules
- Creates IIFE format for browser compatibility
- Generates sourcemaps in development mode
- Minifies in production mode
- Falls back to simple copy if esbuild not installed
Assets are processed from assets/css/ and assets/js/ to the output directory.
Simple key access from :data:
[:eden/get :title] ; Get :title from data
[:eden/get :missing "N/A"] ; With default valueContent namespace special handling:
[:eden/get :content/html] ; Returns RawString for HTMLPath-based access:
[:eden/get-in [:user :name]] ; Nested access
[:eden/get-in [:items 0 :title]] ; Array access
[:eden/get-in [:missing :path] "N/A"] ; With defaultMultiple iteration modes:
;; Simple collection
[:eden/each :items
[:li [:eden/get :name]]]
;; With options
[:eden/each :items
:order-by [:date :desc]
:limit 5
:where {:published true}
[:article ...]]
;; Group by field
[:eden/each :posts
:group-by :category
[:section
[:h2 [:eden/get :eden.each/group-key]]
[:eden/each :eden.each/group-items
[:article ...]]]]
;; All content
[:eden/each :eden/all :where {:type "blog"}
[:article ...]]Special variables:
:eden.each/index- Current index:eden.each/key- Map key (for maps):eden.each/value- Map value (for maps):eden.each/group-key- Group name:eden.each/group-items- Items in group
Handles multiple link types:
;; Direct page link
[:eden/link :about ...]
;; With options
[:eden/link {:content-key :about
:lang :en} ...]
;; Navigation helpers
[:eden/link {:nav :parent} ...] ; Parent page
[:eden/link {:nav :root} ...] ; Home page
;; Dynamic (from :eden/each)
[:eden/link [:eden/get :content-key] ...]Link resolution order:
- Check for standalone page
- Check for section on another page
- Generate warning if not found
Render nested components:
;; Simple
[:eden/render :sidebar]
;; With explicit template
[:eden/render {:data :product.featured
:template :product-card}]
;; With section ID (for anchors)
[:eden/render {:data :products.logifish
:template :product-detail
:section-id "logifish"}]
;; Dynamic (in :eden/each)
[:eden/render {:data [:eden/get :content-key]
:template [:eden/get :template-id]}]Translation with interpolation:
;; Simple
[:eden/t :nav/home]
;; With default
[:eden/t :missing/key "Default text"]
;; With interpolation
[:eden/t :welcome {:name [:eden/get :user-name]}]
; Replaces {{name}} in translation
;; Nested keys
[:eden/t [:errors :not-found]];; Simple boolean
[:eden/if :is-featured
[:span.badge "Featured"]]
;; With else branch
[:eden/if :has-image
[:img {:src [:eden/get :image]}]
[:div.placeholder]]
;; Nested paths
[:eden/if [:user :is-admin]
[:a {:href "/admin"} "Admin"]]Merge map data into context:
[:eden/with :product-details
[:div
[:h3 [:eden/get :name]]
[:p [:eden/get :description]]]]Eden implements the Model Context Protocol for AI integration using clojure-mcp.
The MCP server (eden.mcp) runs as a single process with an embedded nREPL server, providing direct access to Eden's build system and REPL capabilities.
Through clojure-mcp integration, Eden provides:
- File Operations - Read, write, and edit files with Clojure awareness
- REPL Evaluation - Direct access to Eden functions via
clojure_eval - Shell Commands - Execute bash commands in the project context
- Project Navigation - Search and navigate the codebase
The MCP server includes Eden-specific resources and prompts:
-
Resources
- Eden Template Directives documentation
- Content Structure guide
- Site Configuration reference
-
Prompts
eden_project_context- Project understandingeden_create_page- Page creation guideeden_debug_build- Build troubleshooting
MCP also exposes:
- Resources - Direct file access
- Prompts - Guided workflows for common tasks
Eden provides two URL strategies out of the box:
;; :flat strategy
/about → about.html
/blog/post → blog/post.html
;; :nested strategy
/about → about/index.html
/blog/post → blog/post/index.htmlImplement custom strategies as functions:
;; In site.edn
{:url-strategy my.namespace/custom-strategy}
;; In code
(defn custom-strategy [{:keys [path]}]
(str (str/join "/" path) ".html"))Control how links are generated:
;; :default - Clean URLs
/en/about → /en/about
;; :with-extension - Traditional
/en/about → /en/about.html
;; Custom function
(defn custom-page-url [{:keys [slug lang site-config]}]
...)Inject build-time data into all pages:
{:build-constants {:build/time (java.util.Date.)
:build/version "1.2.3"
:build/env "production"}}Access in templates:
[:footer "Built at " [:eden/get :build/time]]Eden collects warnings during build without failing:
{:type :missing-template
:template-name :blog
:content-key :blog.post-1
:location [:template :home]
:message "Template 'blog' not found"}Warning types:
:missing-template- Template not found:missing-content- Content not found:missing-page- Link target not found:ambiguous-link- Multiple link targets:missing-key- Data key not found:unconfigured-language- Language not in config
Uses Hawkeye for file watching:
- Detects changes in content, templates, assets
- Triggers full rebuild on any change
- Build report available at
_report.htmlin output directory
Currently uses browser-sync (npm package) for development server:
- Serves static files from output directory
- Live reload on file changes
- Port 3000 by default
Note: Future versions may switch to embedded Jetty server for better integration.
Add new template directives:
(defmethod sg/process-element :my/directive
[elem context]
...)Integrate asset pipelines:
(defn process-assets [config assets]
;; CSS/JS bundling
;; Image optimization
...)Support new content formats:
(defmethod load-content-file ".yaml"
[file]
(yaml/parse (slurp file)))Test individual components:
(deftest test-eden-link
(testing "basic link generation"
(let [template [:eden/link :about ...]
context {:pages {:about {:slug "about"}}}]
(is (= expected (sg/process template context))))))Test full build pipeline:
(deftest test-full-build
(let [config (load-config "test-site.edn")
result (pipeline/run-build config)]
(is (= 0 (:error-count result)))
(is (fs/exists? "dist/index.html"))))The MCP server enables AI assistants to:
- Validate content syntax before writing
- Preview rendered HTML
- Catch template errors early
- Prevent broken content from being saved
Eden generates standard static HTML files that can be deployed to any static hosting service. The configurable URL strategies (:flat vs :nested) and page URL strategies (:default vs :with-extension) allow you to match the requirements of different hosting platforms.
Common deployment targets include:
- Static site hosts (GitHub Pages, Netlify, Vercel, etc.)
- CDNs with origin storage (CloudFront + S3, etc.)
- Traditional web servers (nginx, Apache, etc.)
Choose the appropriate URL strategy in site.edn based on your hosting platform's requirements.
Eden can be integrated into CI/CD pipelines:
# Example GitHub Actions workflow
- name: Install Eden
run: clj -Ttools install io.github.anteoas/eden '{:git/url "..." :git/tag "v1.0.0"}' :as eden
- name: Build site
run: clj -Teden build :mode '"prod"'FROM clojure:openjdk-17
COPY . /site
WORKDIR /site
RUN clj -Ttools install io.github.anteoas/eden '{:git/url "..." :git/tag "v1.0.0"}' :as eden
RUN clj -Teden build :mode '"prod"'
FROM nginx:alpine
COPY --from=0 /site/dist /usr/share/nginx/htmlConnect to a REPL for interactive development:
(require '[eden.core :as eden])
(eden/build :site-edn "site.edn" :mode :dev)