//! Askama template definitions for all HTML pages and fragments.
//!
//! Split by domain:
//! - `public`: landing, auth, content, blog, discover, health
//! - `dashboard`: creator dashboards, admin, export, account management
//! - `partials`: HTMX fragments, tab content, alerts, form status
mod public;
mod dashboard;
mod partials;
mod embed;
pub use public::*;
pub use dashboard::*;
pub use partials::*;
pub use embed::*;
use askama::Template;
use axum::{
http::StatusCode,
response::{Html, IntoResponse, Response},
};
/// Base context shared by all templates.
/// Note: csrf_token is Option to allow templates to work without CSRF
/// but all authenticated pages should include it.
pub type CsrfTokenOption = Option;
/// One frame of the composable click-through carousel (`partials/carousel.html`).
///
/// A carousel is just an ordered `&[CarouselFrame]`; the same macro renders it
/// on any surface (app product pages, landing) -- only the frame list differs.
/// Build a `Vec` on a page template and pass it to the macro.
///
/// Prefer [`CarouselFrame::new`] over a struct literal: it makes the alt text a
/// required, named argument and nudges (in debug builds) toward alt that
/// actually describes the screenshot. A carousel frame is a meaningful image,
/// so alt is not optional and should not be a filename or a bare label like
/// "screenshot" -- a screen-reader user should get the same information a
/// sighted viewer does.
#[derive(Clone)]
pub struct CarouselFrame {
/// Image URL (typically an optimized screenshot under `/static/images/shots/`).
pub image: String,
/// Alt text describing the screenshot. Required -- every frame is an image.
pub alt: String,
/// Optional caption shown under the frame.
pub caption: Option,
}
impl CarouselFrame {
/// Build a frame, nudging toward helpful alt text.
///
/// In debug builds this asserts the alt text is non-empty and looks like a
/// description rather than a filename or a one-word placeholder. The checks
/// are debug-only so they guide authors during development without ever
/// affecting a release render.
pub fn new(image: impl Into, alt: impl Into) -> Self {
let image = image.into();
let alt = alt.into();
debug_assert!(
!alt.trim().is_empty(),
"carousel frame `{image}` has empty alt text -- describe what the \
screenshot shows so screen-reader users get the same information \
sighted viewers do"
);
debug_assert!(
!alt.trim_start().starts_with('/') && !alt.contains(".webp") && !alt.contains(".png"),
"carousel frame alt text looks like a filename (`{alt}`) -- write a \
human description of what the screenshot shows instead"
);
Self { image, alt, caption: None }
}
/// Attach an optional caption shown under the frame.
#[must_use]
pub fn with_caption(mut self, caption: impl Into) -> Self {
self.caption = Some(caption.into());
self
}
}
/// Helper to convert any Askama template into an Axum response.
fn render_template(template: T) -> Response {
match template.render() {
Ok(html) => Html(html).into_response(),
Err(err) => {
tracing::error!(error = ?err, "template rendering error");
(StatusCode::INTERNAL_SERVER_ERROR, "Template error").into_response()
}
}
}
/// Implement `IntoResponse` for one or more Askama template structs.
macro_rules! impl_into_response {
($($T:ty),+ $(,)?) => {
$(
impl IntoResponse for $T {
fn into_response(self) -> Response {
render_template(self)
}
}
)+
};
}
impl_into_response!(
// Public pages
SandboxTemplate,
PolicyTemplate,
IndexTemplate,
LibraryTemplate,
CartTemplate,
LoginTemplate,
TwoFactorTemplate,
OAuthAuthorizeTemplate,
ForgotPasswordTemplate,
ResetPasswordTemplate,
UserTemplate,
ProjectTemplate,
ProjectPaywallTemplate,
ItemTemplate,
LibraryAudioTemplate,
LibraryDownloadsTemplate,
LibraryLockedTemplate,
LibraryTextTemplate,
LibraryVideoTemplate,
TextReaderTemplate,
AudioPlayerTemplate,
VideoPlayerTemplate,
DiscoverTemplate,
DiscoverResultsTemplate,
PurchaseTemplate,
ReceiptTemplate,
BuyPageTemplate,
FeedTemplate,
StripeConnectDisclaimerTemplate,
// Blog pages
ProjectBlogTemplate,
BlogPostTemplate,
// Documentation pages
DocTemplate,
DocIndexTemplate,
// Pricing calculator
PricingTemplate,
// Platform economics + runway disclosure
EconomicsTemplate,
// Use cases
UseCasesTemplate,
// Team
TeamTemplate,
// Fan+
FanPlusTemplate,
// Creator invite system
CreatorsTemplate,
// Email & account
EmailResultTemplate,
ConfirmDeleteTemplate,
AccountDeletedTemplate,
// Health
HealthTemplate,
// Dashboard pages
DashboardUserTemplate,
DashboardProjectTemplate,
DashboardItemTemplate,
// Admin
AdminWaitlistTemplate,
AdminUsersTemplate,
AdminUploadsTemplate,
AdminScanAuditTemplate,
AdminAppealsTemplate,
AdminReportsTemplate,
AdminSignupsTemplate,
AdminMetricsTemplate,
// Export, import & account management
ExportPortalTemplate,
ImportPortalTemplate,
DeleteAccountTemplate,
BlogEditorTemplate,
// HTMX partials
AlertTemplate,
LibraryStatusTemplate,
ExportDownloadTemplate,
ExportContentReadyTemplate,
TransactionsTableTemplate,
UserProfileTabTemplate,
UserSettingsTabTemplate,
UserAccountTabTemplate,
UserSshKeysTabTemplate,
UserPaymentsTabTemplate,
UserProjectsTabTemplate,
UserCreatorTabTemplate,
ProjectOverviewTabTemplate,
ProjectContentTabTemplate,
ProjectAnalyticsTabTemplate,
UserAnalyticsTabTemplate,
BuyerContactsPartialTemplate,
ProjectSettingsTabTemplate,
ProjectCodeTabTemplate,
ProjectBlogTabTemplate,
ProjectSubscriptionsTabTemplate,
ProjectMembersTabTemplate,
ProjectMonetizationTabTemplate,
ItemEditRowTemplate,
// Admin partials
AdminWaitlistEntriesTemplate,
AdminUserEntriesTemplate,
AdminUploadEntriesTemplate,
AdminQueueSummaryTemplate,
AdminAppealEntriesTemplate,
AdminReportEntriesTemplate,
SuspensionBannerTemplate,
// License keys
ItemLicenseKeysTemplate,
// Promo codes
PromoCodesListTemplate,
ProjectPromotionsTabTemplate,
// Sessions
UserSessionsPartialTemplate,
// SyncKit
UserSyncKitTabTemplate,
ProjectSyncKitTabTemplate,
// Forums (Multithreaded)
UserForumsTabTemplate,
// Media library
UserMediaTabTemplate,
// Support
UserSupportTabTemplate,
// Collections
CollectionTemplate,
// Library tabs
LibraryPurchasesTabTemplate,
LibraryFeedTabTemplate,
LibraryCollectionsTabTemplate,
LibraryContactsTabTemplate,
LibraryCommunitiesTabTemplate,
// Follow button
FollowButtonTemplate,
TagFollowToggleTemplate,
// Tag suggestions
TagSuggestionsTemplate,
// Item analytics
ItemAnalyticsPartialTemplate,
// Item dashboard tabs
ItemOverviewTabTemplate,
ItemDetailsTabTemplate,
ItemPricingTabTemplate,
ItemFilesTabTemplate,
ItemSalesTabTemplate,
ItemEmbedTabTemplate,
// Onboarding checklist
OnboardingChecklistPartialTemplate,
// Tag tree browser
TagTreeTemplate,
// TOTP 2FA
TotpSetupTemplate,
TotpStatusTemplate,
// Passkeys
PasskeyListTemplate,
// Git source browser
GitRepoTemplate,
GitTreeTemplate,
GitFileTemplate,
GitCommitsTemplate,
GitCommitDetailTemplate,
GitBlameTemplate,
GitUserReposTemplate,
GitExploreTemplate,
GitFileLogTemplate,
// Git issues
GitIssueListTemplate,
GitIssueDetailTemplate,
GitRepoSettingsTemplate,
// Join wizard
WizardJoinTemplate,
WizardJoinAccountTemplate,
WizardJoinProfileTemplate,
WizardJoinCompleteTemplate,
// Creation wizards — full pages
WizardProjectTemplate,
WizardItemTemplate,
// Creation wizards — project step partials
WizardProjectBasicsTemplate,
WizardProjectAppearanceTemplate,
WizardProjectMonetizationTemplate,
WizardProjectFirstContentTemplate,
WizardProjectPreviewTemplate,
// Creation wizards — item step partials
WizardItemTypeTemplate,
WizardItemBasicsTemplate,
WizardItemContentTemplate,
WizardItemSectionsTemplate,
WizardItemPricingTemplate,
WizardItemPreviewTemplate,
// Embed widgets
EmbedItemButtonTemplate,
EmbedItemCardTemplate,
EmbedItemPlayerTemplate,
EmbedTipButtonTemplate,
EmbedProjectCardTemplate,
);