Arizona is a modern, real-time web framework for Erlang that delivers high-performance web applications through compile-time optimization and differential rendering. Built for scalability and developer experience, it combines the reliability of Erlang/OTP with modern web development paradigms.
Work in progress.
Use it at your own risk, as the API may change at any time.
Arizona follows a component-based architecture with three main layers:
- Compile-time optimization via parse transforms for maximum performance
- Template DSL with plain HTML,
{}
Erlang expressions, and\{
escaping for literal braces - GitHub Flavored Markdown support with template syntax preservation for content-driven applications
- Differential rendering for minimal DOM updates
- Static site generation support for SEO and deployment flexibility
- Views - Top-level page components with full lifecycle management (require
id
field) - Stateful Components - Interactive components with persistent state (require
id
field) - Stateless Components - Pure rendering functions for simple UI elements
- Layouts - Document wrappers with slot-based content insertion
- Collections - List and map rendering with optimized iteration patterns
- WebSocket transport with JSON message protocol
- Live processes managing connected clients with state synchronization
- PubSub system for decoupled inter-process communication
- Differential updates minimizing network traffic through smart diffing
- Real-time WebSocket updates with automatic client synchronization
- Hierarchical component rendering supporting complex nested structures
- Compile-time template optimization for maximum runtime performance
- Type-safe stateful and stateless components with behavior validation
- Efficient differential DOM updates reducing browser workload
- Simple template syntax using plain HTML with
{}
Erlang expressions - GitHub Flavored Markdown processing with Arizona template integration
- Static site generation for deployment flexibility and SEO optimization
- File watching infrastructure for custom development automation
- Unified middleware system for request processing across all route types
- Simple plugin system for configuration transformation and extensibility
In your rebar.config
:
{deps, [
{arizona, {git, "https://github.com/arizona-framework/arizona", {branch, "main"}}}
]}.
-module(home_view).
-behaviour(arizona_view).
-compile({parse_transform, arizona_parse_transform}).
-export([mount/2, render/1, handle_event/3]).
mount(_Arg, _Request) ->
arizona_view:new(
?MODULE,
#{
id => ~"home",
greeting => ~"Hello, Arizona!",
count => 0
},
none % No layout
).
render(Bindings) ->
arizona_template:from_html(~"""
<div id="{arizona_template:get_binding(id, Bindings)}">
<h1>{arizona_template:get_binding(greeting, Bindings)}</h1>
<p>Clicked: {arizona_template:get_binding(count, Bindings)} times</p>
<button onclick="arizona.pushEvent('increment')">Click me</button>
</div>
""").
handle_event(~"increment", _Params, View) ->
State = arizona_view:get_state(View),
Count = arizona_stateful:get_binding(count, State),
NewState = arizona_stateful:put_binding(count, Count + 1, State),
{[], arizona_view:update_state(NewState, View)}.
Create config/sys.config
:
[
{arizona, [
{server, #{
enabled => true,
scheme => http, % or https
transport_opts => [{port, 1912}], % Cowboy/Ranch transport options
proto_opts => #{env => #{custom_option => value}}, % Optional Cowboy protocol options
routes => [
{view, ~"/", home_view, #{}, []}, % Empty middleware list
{websocket, ~"/live", #{}, []}, % WebSocket options + middleware list
{controller, ~"/api/presence", my_api_controller, #{}, []}, % Plain Cowboy handler
{asset, ~"/assets", {priv_dir, arizona, ~"static/assets"}, []} % Required for live features
]
}},
{reloader, #{
enabled => true, % Enable file watcher coordination
rules => [
#{
handler => {my_erlang_handler, #{profile => dev}}, % {Module, Options}
watcher => #{
directories => ["src"],
patterns => [".*\\.erl$"],
debounce_ms => 100
}
}
]
}}
]}
].
Then start the application:
{ok, _Started} = application:ensure_all_started(arizona).
Note
Add to your rebar.config
to automatically load the config with rebar3 shell
:
{shell, [{config, "config/sys.config"}]}.
% Configure the application environment
application:set_env([{arizona, [
{server, #{
enabled => true,
scheme => http,
transport_opts => [{port, 1912}],
routes => [
{view, ~"/", home_view, #{}, []},
{websocket, ~"/live", #{}, []},
{asset, ~"/assets", {priv_dir, arizona, ~"static/assets"}, []}
]
}},
{reloader, #{enabled => false}} % Typically disabled when using programmatic config
]}]),
% Start the application
{ok, _Started} = application:ensure_all_started(arizona).
% You must implement your own handler modules:
% Example:
-module(my_erlang_handler).
-behaviour(arizona_reloader).
-export([reload/2]).
reload(Files, Options) ->
Profile = maps:get(profile, Options, default),
io:format("Compiling ~p with profile ~p~n", [Files, Profile]),
% Your custom build logic here
ok.
Note: The asset route
{asset, ~"/assets", {priv_dir, arizona, ~"static/assets"}}
is required for Arizona's WebSocket/live functionality to work as it serves the necessary JavaScript client files.
Try the complete working examples:
# Clone and run the test server
$ git clone https://github.com/arizona-framework/arizona
$ cd arizona
$ ./scripts/start_test_server.sh
Then visit:
- http://localhost:8080/counter - Simple stateful interactions
- http://localhost:8080/todo - CRUD operations and list management
- http://localhost:8080/modal - Dynamic overlays and slot composition
- http://localhost:8080/datagrid - Sortable tables with complex data
- http://localhost:8080/realtime - Real-time PubSub updates and live data
- http://localhost:8080/blog - Markdown template processing with Arizona syntax
- http://localhost:8080/ - Static blog home page
- http://localhost:8080/about - Static about page
- http://localhost:8080/post/hello-world - Dynamic blog post routing
- http://localhost:8080/post/arizona-static - Blog post about Arizona static generation
Each demo corresponds to complete source code in test/support/e2e/
:
- Counter App
- Layout + View with event handling and PubSub integration
- Todo App
- Complex state management and CRUD operations
- Modal System
- Component composition, slots, and dynamic overlays
- Data Grid
- Advanced data presentation and sorting functionality
- Real-time App
- Live data updates and WebSocket communication
- Blog App
- Markdown template processing with Arizona syntax integration
- Static Blog
- Static site generation with layouts and dynamic routing
For additional examples and real-world usage patterns, check out the dedicated example repository:
- Arizona Example
- Complete application examples and patterns
Arizona templates combine HTML with Erlang expressions using a simple and intuitive syntax:
% Inline HTML template
arizona_template:from_html(~"""
<div>
<h1>{arizona_template:get_binding(title, Bindings)}</h1>
<p>User: {arizona_template:get_binding(username, Bindings)}</p>
</div>
""")
% File-based template with compile-time optimization
arizona_template:from_html({file, "templates/user.html"})
arizona_template:from_html({priv_file, myapp, "templates/user.html"})
Render interactive components that maintain their own state between updates:
arizona_template:from_html(~"""
<div>
{arizona_template:render_stateful(counter_component, #{
id => ~"my_counter",
count => 0
})}
</div>
""")
Render pure functions that return templates based solely on their input:
arizona_template:from_html(~"""
<div>
{arizona_template:render_stateless(my_module, render_header, #{
title => ~"Welcome",
user => ~"John"
})}
</div>
""")
Iterate over lists with automatic compile-time optimization via parse transforms:
arizona_template:from_html(~"""
<ul>
{arizona_template:render_list(fun(Item) ->
arizona_template:from_html(~"""
<li>{arizona_template:get_binding(name, Item)}</li>
""")
end, arizona_template:get_binding(items, Bindings))}
</ul>
""")
Iterate over key-value pairs with optimized rendering performance:
arizona_template:from_html(~"""
<div>
{arizona_template:render_map(fun({Key, Value}) ->
arizona_template:from_html(~"""
<p>{Key}: {Value}</p>
""")
end, #{~"name" => ~"Arizona", ~"type" => ~"Framework"})}
</div>
""")
Insert dynamic content into predefined slots for flexible component composition:
arizona_template:from_html(~"""
<div class="modal">
<h1>{arizona_template:render_slot(arizona_template:get_binding(header, Bindings))}</h1>
<div class="content">
{arizona_template:render_slot(arizona_template:get_binding(inner_block, Bindings))}
</div>
</div>
""")
Use template comments and escape braces for literal output in CSS or JavaScript:
arizona_template:from_html(~"""
<div>
{% This is a comment and is not rendered }
<h1>{arizona_template:get_binding(title, Bindings)}</h1>
<style>
.css-rule \{ color: blue; } /* \{ renders as literal { */
</style>
</div>
""")
Arizona includes GitHub Flavored Markdown processing with full template syntax preservation for content-driven applications.
- Full GFM: Tables, autolinks, strikethrough, task lists, tag filtering
- Template Integration:
{expressions}
and%
comments work within markdown - High Performance: Built on
cmark-gfm
C library via NIF - Production Ready: Safety limits, error handling, comprehensive tests
% Pure markdown to HTML
{ok, Html} = arizona_markdown:to_html(~"# Hello **World**")
% Returns: ~"<h1>Hello <strong>World</strong></h1>\n"
% Inline markdown template
arizona_template:from_markdown(~"""
# {arizona_template:get_binding(title, Bindings)}
Welcome **{arizona_template:get_binding(user, Bindings)}**!
{% Template comment - not rendered %}
{arizona_template:render_stateful(my_widget_component, #{
id => ~"widget",
data => arizona_template:get_binding(widget_data, Bindings)
})}
""")
% File-based markdown template with compile-time optimization
arizona_template:from_markdown({file, "content/blog-post.md"})
arizona_template:from_markdown({priv_file, myapp, "content/blog-post.md"})
% With markdown options
{ok, Html} = arizona_markdown:to_html(~"# Hello", [source_pos, smart])
render(Bindings) ->
arizona_template:from_markdown(~"""
# {arizona_template:get_binding(title, Bindings)}
{arizona_template:get_binding(content, Bindings)}
{arizona_template:render_stateful(comment_section, #{
id => ~"comments",
post_id => arizona_template:get_binding(id, Bindings)
})}
""").
Arizona follows a hierarchical component model with clear separation of concerns:
Top-level page components that represent complete routes. Views:
- Require an
id
field in their bindings for internal state management and component tracking - Initialize from mount arguments and HTTP requests via
mount/2
- Manage their own state plus nested stateful components
- Handle WebSocket events via
handle_event/3
- Support optional layout wrapping
- Generate templates via
render/1
Interactive components with persistent internal state and full lifecycle management. They:
- Require an
id
field in their bindings for state management, event routing, and lifecycle tracking - Mount with initial bindings via
mount/1
- Maintain state between renders for efficient diff updates
- Handle events independently via
handle_event/3
- Track changes for differential DOM updates
- Support automatic cleanup via optional
unmount/1
callback when removed from component tree
- Mount: Component initialized with
mount/1
using initial bindings - Render: Template generated with
render/1
using current state - Events: User interactions processed via
handle_event/3
- Updates: State changes trigger re-rendering with minimal DOM updates
- Unmount: Optional cleanup via
unmount/1
when component removed from template
The unmount/1
callback is automatically called when:
- Parent template changes and component is no longer rendered
- Component is replaced with a different component at the same location
- View navigation removes the entire component tree
Use unmount/1
for cleanup tasks such as:
- Canceling timers and intervals
- Unsubscribing from PubSub topics
- Closing network connections
- Releasing GenServer references
- Cleaning up ETS tables or other shared resources
-module(timer_component).
-behaviour(arizona_stateful).
-export([mount/1, render/1, handle_event/3, unmount/1]).
mount(Bindings) ->
% Start a timer when component mounts
{ok, TimerRef} = timer:send_interval(1000, tick),
arizona_pubsub:join(~"time_updates", self()),
NewBindings = Bindings#{timer_ref => TimerRef, seconds => 0},
arizona_stateful:new(?MODULE, NewBindings).
handle_event(~"tick", _Params, State) ->
Seconds = arizona_stateful:get_binding(seconds, State),
NewState = arizona_stateful:put_binding(seconds, Seconds + 1, State),
{[], NewState}.
% Automatic cleanup when component is unmounted
unmount(State) ->
% Cancel timer to prevent memory leaks
case arizona_stateful:get_binding(timer_ref, State) of
undefined -> ok;
TimerRef -> timer:cancel(TimerRef)
end,
% Unsubscribe from PubSub
arizona_pubsub:leave(~"time_updates", self()),
ok.
Pure rendering functions that:
- Accept bindings and return templates
- Have no internal state or lifecycle
- Are deterministic based solely on input
- Provide reusable UI elements
Document-level wrappers that:
- Define HTML structure and metadata
- Enable consistent page structure across views
- Handle head section and asset loading
- Use slots for dynamic content insertion
- Never re-rendered - layouts are static and no diffs are generated for them
Example layout with JavaScript client setup:
-module(my_layout).
-compile({parse_transform, arizona_parse_transform}).
-export([render/1]).
render(Bindings) ->
arizona_template:from_html(~"""
<!DOCTYPE html>
<html>
<head>
<title>My Arizona App</title>
<script type="module" async>
import { Arizona, ArizonaConsoleLogger, LOG_LEVELS } from '@arizona-framework/client';
// Create client with optional logger
const logger = new ArizonaConsoleLogger({ logLevel: LOG_LEVELS.info });
globalThis.arizona = new Arizona({ logger });
arizona.connect('/live');
</script>
</head>
<body>
{arizona_template:render_slot(arizona_template:get_binding(main_content, Bindings))}
</body>
</html>
""").
All components support slot-based composition:
- Accept dynamic content via
arizona_template:render_slot/1
- Support view references, templates, or HTML values
- Enable flexible component composition and reuse
- Used in layouts for content insertion and views for dynamic sections
Arizona uses a flexible action system for handling callback responses. All callbacks return
{Actions, State}
where Actions
is a list of action tuples.
% No action - just update state
{[], NewState}
% Dispatch custom events - subscribe using arizona.on('event_name', callback)
{[{dispatch, ~"dataLoaded", #{status => success, data => Value}}], NewState}
{[{dispatch, ~"notification:show", #{message => ~"Success!"}}], NewState}
{[{dispatch, ~"counter_123:update", #{count => 5}}], NewState}
% Redirect to new URL
{[{redirect, ~"/new-page", #{target => ~"_self"}}], NewState} % Same tab
{[{redirect, ~"/external", #{target => ~"_blank"}}], NewState} % New tab
% Redirect with window features
{[{redirect, ~"/popup", #{
target => ~"popup_window",
window_features => ~"width=600,height=400,resizable=yes"
}}], NewState}
% Reload the current page
{[reload], NewState}
% Multiple actions - executed in sequence
{[
{dispatch, ~"taskCompleted", #{message => ~"Saved successfully!"}},
{redirect, ~"/dashboard", #{target => ~"_self"}}
], NewState}
- Multiple Responses: Send multiple actions per callback
- Built-in Functionality: No need to implement redirects or reloads manually
- Custom Event Dispatching: Integrate with any JavaScript framework or library via client events
- Consistent API: Same pattern across views and stateful components
- Type Safety: All actions are validated and processed uniformly
- Future Extensible: Easy to add new action types as needed
Arizona includes a unified middleware system that works consistently across all route types (view, controller, websocket, and asset). Middlewares process requests before they reach the main handler, enabling powerful features like authentication, authorization, CORS handling, request modification, and custom response generation.
Note
Arizona does not provide any built-in middleware implementations. All middleware examples shown below are for demonstration purposes and must be implemented by you according to your application's specific requirements.
All routes support middleware lists as their final parameter:
routes => [
% View routes with authentication middleware
{view, ~"/admin", admin_view, #{}, [
{auth_middleware, #{jwt_secret => ~"secret123", redirect_on_fail => true}},
{role_middleware, #{required_role => admin}}
]},
% Controller routes with CORS and auth
{controller, ~"/api/users", users_controller, #{}, [
{cors_middleware, #{origins => [~"https://app.com"]}},
{auth_middleware, #{jwt_secret => ~"secret123"}}
]},
% WebSocket routes with authentication
{websocket, ~"/live", #{idle_timeout => 60000}, [
{auth_middleware, #{jwt_secret => ~"secret123"}}
]},
% Asset routes with authentication (for protected files)
{asset, ~"/private", {priv_dir, myapp, ~"private"}, [
{auth_middleware, #{jwt_secret => ~"secret123"}}
]},
% Public routes with no middlewares
{view, ~"/public", public_view, #{}, []}
]
Implement the arizona_middleware
behavior:
-module(auth_middleware).
-behaviour(arizona_middleware).
-export([execute/2]).
execute(Req, #{jwt_secret := Secret} = Opts) ->
case cowboy_req:header(~"authorization", Req) of
<<"Bearer ", Token/binary>> ->
case validate_jwt(Token, Secret) of
{ok, Claims} ->
% Add user info for downstream middlewares/handlers
Req1 = cowboy_req:set_meta(user_claims, Claims, Req),
{continue, Req1};
{error, _} ->
Req1 = cowboy_req:reply(401, #{}, ~"Invalid token", Req),
{halt, Req1}
end;
undefined ->
case maps:get(redirect_on_fail, Opts, false) of
true ->
% Redirect to login for views
Req1 = cowboy_req:reply(302, #{~"location" => ~"/login"}, ~"", Req),
{halt, Req1};
false ->
% JSON error for APIs
Req1 = cowboy_req:reply(401, #{}, ~"Unauthorized", Req),
{halt, Req1}
end
end.
validate_jwt(_Token, _Secret) ->
% Your JWT validation logic
{ok, #{user_id => ~"123", role => ~"admin"}}.
- Middlewares execute sequentially in the order specified
- Each middleware returns
{continue, Req1}
or{halt, Req1}
- If any middleware returns
{halt, Req1}
, processing stops (response already sent) - If all middlewares return
{continue, Req1}
, the request reaches the main handler
Arizona integrates middlewares into Cowboy's pipeline as a generic middleware:
cowboy_router -> arizona_middleware_cowboy -> cowboy_handler
This ensures proper integration with Cowboy's request processing while maintaining Arizona's flexibility and performance.
Arizona includes a simple plugin system that allows you to transform the server configuration before startup. Plugins are perfect for adding middleware, modifying routes, or customizing server behavior across your application.
Note
Arizona does not provide any built-in plugins. All plugin examples shown below are for demonstration purposes and must be implemented according to your application's requirements.
Implement the arizona_plugin
behavior:
-module(my_auth_plugin).
-behaviour(arizona_plugin).
-export([transform_config/2]).
transform_config(Config, PluginConfig) ->
JwtSecret = maps:get(jwt_secret, PluginConfig),
% Transform routes to add auth middleware to protected paths
ServerConfig = maps:get(server, Config),
Routes = maps:get(routes, ServerConfig, []),
NewRoutes = lists:map(fun(Route) ->
case Route of
{view, Path, ViewModule, MountArg, Middlewares} when
binary:match(Path, <<"admin">>) =/= nomatch ->
% Add auth middleware to admin routes
AuthMiddleware = {auth_middleware, #{jwt_secret => JwtSecret}},
{view, Path, ViewModule, MountArg, [AuthMiddleware | Middlewares]};
Other ->
Other
end
end, Routes),
UpdatedServerConfig = ServerConfig#{routes => NewRoutes},
Config#{server => UpdatedServerConfig}.
Configure plugins in your sys.config
:
[
{arizona, [
{server, #{
enabled => true,
transport_opts => [{port, 1912}],
routes => [
{view, ~"/", home_view, #{}, []},
{view, ~"/admin", admin_view, #{}, []}, % Plugin will add auth middleware
{controller, ~"/api/users", users_controller, #{}, []}
]
}},
% Configure plugins with their options
{plugins, [
{my_auth_plugin, #{jwt_secret => "secret123"}},
{my_cors_plugin, #{origins => ["*"]}},
{my_logging_plugin, true} % Plugin config can be any term
]}
]}
].
- Plugins execute before server startup in the order specified
- Each plugin receives the current config and returns the transformed config
- Plugin configuration can be any Erlang term (maps, lists, atoms, tuples, etc.)
- If a plugin fails, Arizona startup fails with a clear error message
- Authentication: Automatically add auth middleware to protected routes
- CORS: Add CORS headers to API routes
- Logging: Add request logging middleware to all routes
- Rate Limiting: Add rate limiting middleware based on route patterns
- Environment Configuration: Modify routes or options based on deployment environment
Plugins are distributed as standard Erlang applications via Hex.pm:
% rebar.config
{deps, [
{arizona, "~> 1.0"},
{arizona_auth_plugin, "~> 1.0"} % Add plugin dependency
]}.
% sys.config - just reference by name
{plugins, [
{arizona_auth_plugin, #{jwt_secret => "secret123"}}
]}
Arizona provides multiple ways to handle user interactions and real-time updates:
% In templates - send events to current view
<button onclick="arizona.pushEvent('my_event')">Click</button>
% Send events to specific components
<button onclick="arizona.pushEventTo('component_id', 'increment', \{amount: 5})">+5</button>
% In view/component modules - handle events
handle_event(~"my_event", Params, State) ->
% Update state and return {Actions, NewState} where Actions is a list
{[], arizona_stateful:put_binding(updated, true, State)}.
% Example with actions - dispatch custom event to client
handle_event(~"save_data", Params, State) ->
% Process data and dispatch event to client
{[{dispatch, ~"dataSaved", #{status => success, id => 123}}], UpdatedState}.
% Example with redirect action
handle_event(~"login_success", _Params, State) ->
% Redirect user to dashboard after login
{[{redirect, ~"/dashboard", #{target => ~"_self"}}], State}.
% Example with reload action
handle_event(~"reset_app", _Params, State) ->
% Reload the entire page
{[reload], State}.
% Example with multiple actions
handle_event(~"complete_task", _Params, State) ->
% Dispatch event and then redirect
Actions = [
{dispatch, ~"taskCompleted", #{message => ~"Task completed!"}},
{redirect, ~"/tasks", #{target => ~"_self"}}
],
{Actions, State}.
Subscribe to topics, broadcast messages, and handle real-time updates:
% Subscribe to topics during mount
mount(_Arg, _Request) ->
case arizona_live:is_connected(self()) of
true -> arizona_pubsub:join(~"time_update", self());
false -> ok
end,
arizona_view:new(?MODULE, #{current_time => ~"Loading..."}).
% Publish messages from external processes
arizona_pubsub:broadcast(~"time_update", #{~"time" => TimeString}),
% Handle PubSub messages in views (treated as events)
handle_event(~"time_update", Data, View) ->
NewTime = maps:get(~"time", Data),
State = arizona_view:get_state(View),
UpdatedState = arizona_stateful:put_binding(current_time, NewTime, State),
{[], arizona_view:update_state(UpdatedState, View)}.
% Views can handle arbitrary Erlang messages
handle_info({timer, update}, View) ->
% Handle timer or other process messages
{[], UpdatedView}.
Arizona provides a built-in event subscription system that allows seamless integration with any JavaScript framework or library.
Subscribe to events triggered by dispatch
actions from handle_event/3
:
// Subscribe to custom events dispatched from the server
const unsubscribe = arizona.on('dataSaved', (data) => {
// Handle data saved event with custom data
console.log('Data saved:', data);
showNotification('Saved successfully!');
});
// Component-scoped events using namespace pattern
arizona.on('notification:show', (data) => {
const notification = document.querySelector('#notification');
notification.textContent = data.message;
notification.classList.add('visible');
});
// Multiple component instances with unique IDs
arizona.on('counter_123:update', (data) => {
document.querySelector('#counter_123 .count').textContent = data.count;
});
arizona.on('counter_456:update', (data) => {
document.querySelector('#counter_456 .count').textContent = data.count;
});
// Unsubscribe when no longer needed
const cleanup = arizona.on('myEvent', handleEvent);
cleanup(); // Removes the event listener
// Subscribe to Arizona framework events
arizona.on('connected', (data) => {
showConnectionIndicator('online');
});
arizona.on('disconnected', (data) => {
showConnectionIndicator('offline');
});
arizona.on('error', (data) => {
console.error('Server error:', data.error);
showErrorMessage(data.error);
});
// Example: Handle form submission with custom event feedback
function submitForm(formData) {
// Send event to server
arizona.pushEvent('submit_form', formData);
// Listen for response (one-time subscription)
const cleanup = arizona.on('formSubmitted', (data) => {
if (data.success) {
showSuccess('Form submitted successfully!');
}
cleanup(); // Clean up after handling
});
}
Arizona provides file watching tools that you can use to build custom development automation:
- Generic File Watcher:
arizona_watcher
GenServer for monitoring directories - Watcher Supervisor:
arizona_watcher_sup
for managing multiple watcher instances - Reloader Coordination:
arizona_reloader
system for organizing multiple handlers - Custom Handler Pattern: Implement your own reload/build/compilation logic
Arizona does not provide any built-in reloading functionality. You must implement your own handler modules with custom logic for compilation, hot-loading, asset building, etc.
Example handler implementing the arizona_reloader
behavior:
-module(my_custom_handler).
-behaviour(arizona_reloader).
-export([reload/2]).
reload(Files, Options) ->
% Your custom logic: compile, build, reload, etc.
io:format("Files changed: ~p with options: ~p~n", [Files, Options]),
% You implement what happens here
ok.
Key Point: The reloader system is just infrastructure. All actual reloading, compilation, and automation logic is your responsibility to implement in handler modules.
Arizona's JavaScript client includes intelligent reload handling that optimizes the development experience:
- CSS-only reloading: When CSS files change, only stylesheets are refreshed without full page reload
- Application state preservation: Form inputs, scroll position, and component state remain intact during CSS updates
- Automatic fallback: Non-CSS file changes trigger full page reload as expected
- File type detection: Reload handlers can specify
file_type
to control client behavior
When implementing custom reload handlers, you can leverage this feature:
% In your reload handler, specify file type for smart client handling
reload(_Files, _Options) ->
% Compile logic here
FileType = css,
arizona_pubsub:broadcast(~"arizona:reload", FileType).
This enhancement significantly improves the development workflow by avoiding unnecessary page reloads during CSS development.
Arizona's JavaScript client provides a pluggable logging system with log levels aligned with Erlang's logger for consistent debugging across the stack.
Basic Usage:
import { Arizona, ArizonaConsoleLogger, LOG_LEVELS } from '@arizona-framework/client';
// Production - no logger (silent by default)
const arizona = new Arizona();
// Development - console logger with info level
const arizona = new Arizona({
logger: new ArizonaConsoleLogger({ logLevel: LOG_LEVELS.info })
});
// Full debugging - all internal operations
const arizona = new Arizona({
logger: new ArizonaConsoleLogger({ logLevel: LOG_LEVELS.debug })
});
// Programmatic control
const arizona = new Arizona({
logger: process.env.NODE_ENV === 'development'
? new ArizonaConsoleLogger({ logLevel: LOG_LEVELS.debug })
: null
});
Available Log Levels (aligned with Erlang logger):
LOG_LEVELS.error
(3): Errors onlyLOG_LEVELS.warning
(4): Warnings and errorsLOG_LEVELS.info
(6): Connection status, reload notifications, redirects (default)LOG_LEVELS.debug
(7): All internal operations and message details
Custom Logger:
Implement your own logger by extending ArizonaLogger
:
import { ArizonaLogger, LOG_LEVELS } from '@arizona-framework/client';
class MyCustomLogger extends ArizonaLogger {
handleLog(level, message, ...args) {
// Send to your logging service, format differently, etc.
fetch('/api/logs', {
method: 'POST',
body: JSON.stringify({ level, message, args })
});
}
}
const arizona = new Arizona({
logger: new MyCustomLogger({ logLevel: LOG_LEVELS.warning })
});
Benefits:
- Optional: Logger is completely optional - production apps can omit it entirely
- Pluggable: Use built-in console logger or implement custom logging backends
- Flexible development: Choose appropriate verbosity for debugging
- Erlang alignment: Log levels match backend for consistent configuration
- Zero overhead: No logger means zero logging overhead
Generate static HTML for deployment:
arizona_static:generate(#{
route_paths => #{
~"/" => #{},
~"/about" => #{parallel => true},
~"/posts/123" => #{parallel => false}
},
output_dir => ~"_site"
}).
Configure your production config/sys.config
:
[
{arizona, [
{server, #{
enabled => true,
scheme => https, % Use HTTPS in production
transport_opts => #{
socket_opts => [{port, 443}],
% Add SSL certificates and options
ssl_opts => [
{certfile, "/path/to/cert.pem"},
{keyfile, "/path/to/key.pem"}
]
},
proto_opts => #{
env => #{
max_keepalive => 100,
timeout => 60000
}
},
routes => YourRoutes
}},
{reloader, #{enabled => false}} % Disable file watchers in production
]}
].
Then start normally:
{ok, _Started} = application:ensure_all_started(arizona).
Install the Arizona client via npm:
npm install @arizona-framework/client
Import Options:
// Recommended: Import everything from main entry point
import { Arizona, ArizonaConsoleLogger, LOG_LEVELS } from '@arizona-framework/client';
// Or: Import from specific subpaths
import { Arizona } from '@arizona-framework/client';
import { ArizonaConsoleLogger } from '@arizona-framework/client/logger';
import ArizonaConsoleLogger from '@arizona-framework/client/logger/console';
Asset Route (Optional):
If you're not using the npm package and serving Arizona's JavaScript files directly from
the priv/static/assets
directory, you need this asset route:
{asset, ~"/assets", {priv_dir, arizona, ~"static/assets"}, []}
When using the npm package with a bundler (Vite, Webpack, esbuild, etc.), this route is not required as your bundler will handle the JavaScript files.
Static Site Generation:
Use arizona_static:generate/1
to generate static HTML files from your views.
Static generation creates SEO-friendly HTML that can be deployed to any web server.
- Arizona templates are compile-time optimized via parse transforms
- Differential rendering minimizes WebSocket traffic
- Live processes are lightweight Erlang processes
- PubSub uses Erlang's built-in
pg
for efficient message routing
Arizona provides additional tools to enhance the development experience:
- arizona.nvim - Neovim plugin for Arizona development
- tree-sitter-arizona - Tree-sitter grammar for Arizona templates
- Erlang/OTP 28+
If you like this tool, please consider sponsoring me. I'm thankful for your never-ending support ❤️
I also accept coffees ☕
Contributions are welcome! Please see CONTRIBUTING.md for development setup, testing guidelines, and contribution workflow.
Copyright (c) 2023-2025 William Fank Thomé
Arizona is 100% open-source and community-driven. All components are available under the Apache 2 License on GitHub.
See LICENSE.md for more information.