|
10 | 10 | ;; MAILGUN_LIST_ID (example: "my@list.com") |
11 | 11 | ;; MAILGUN_API_ENDPOINT (example "https://api.eu.mailgun.net/v3") |
12 | 12 | ;; MAILGUN_API_KEY (example "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxx-xxxxxxxx" |
| 13 | +;; APP_BASE_PATH (optional, example: "/app" - for subdirectory deployments) |
13 | 14 | ;; |
14 | 15 | ;; Running the web application as http://localhost:8080 |
15 | 16 | ;; |
|
40 | 41 | :appenders {:println {:min-level :debug |
41 | 42 | :fn #(println %)}}}) |
42 | 43 |
|
| 44 | +;; Base path configuration for subdirectory deployments |
| 45 | +(def base-path |
| 46 | + (let [path (or (System/getenv "APP_BASE_PATH") "")] |
| 47 | + (if (str/blank? path) |
| 48 | + "" |
| 49 | + (if (str/ends-with? path "/") |
| 50 | + (str/replace path #"/$" "") ;; Remove trailing slash |
| 51 | + path)))) |
| 52 | + |
| 53 | +(log/info "APP_BASE_PATH:" (if (str/blank? base-path) "[not set]" base-path)) |
| 54 | + |
| 55 | +;; Helper function to construct paths with the base path |
| 56 | +(defn make-path [& segments] |
| 57 | + (let [segments (remove str/blank? segments)] |
| 58 | + (str base-path |
| 59 | + (if (and (not (str/blank? base-path)) |
| 60 | + (not (str/starts-with? (first segments) "/"))) |
| 61 | + "/" |
| 62 | + "") |
| 63 | + (str/join "/" segments)))) |
| 64 | + |
43 | 65 | ;; Default language setting |
44 | 66 | (def default-language :en) |
45 | 67 |
|
|
327 | 349 | <meta charset=\"UTF-8\"> |
328 | 350 | <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> |
329 | 351 | <title>%s</title> |
330 | | - |
| 352 | +
|
331 | 353 | <!-- DSFR resources --> |
332 | 354 | <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@gouvfr/dsfr@1.11.0/dist/dsfr/dsfr.min.css\"> |
333 | 355 | <link rel=\"apple-touch-icon\" href=\"https://cdn.jsdelivr.net/npm/@gouvfr/dsfr@1.11.0/dist/favicon/apple-touch-icon.png\"> |
334 | 356 | <link rel=\"icon\" href=\"https://cdn.jsdelivr.net/npm/@gouvfr/dsfr@1.11.0/dist/favicon/favicon.svg\" type=\"image/svg+xml\"> |
335 | 357 | <link rel=\"shortcut icon\" href=\"https://cdn.jsdelivr.net/npm/@gouvfr/dsfr@1.11.0/dist/favicon/favicon.ico\" type=\"image/x-icon\"> |
336 | | - |
| 358 | +
|
337 | 359 | <!-- HTMX for form interactions --> |
338 | 360 | <script src=\"https://unpkg.com/htmx.org@1.9.6\"></script> |
339 | | - |
| 361 | +
|
340 | 362 | <style> |
341 | 363 | .success { |
342 | 364 | border-left: 5px solid var(--success-425-625); |
343 | 365 | padding: 1rem; |
344 | 366 | margin-bottom: 1rem; |
345 | 367 | background-color: var(--success-950-100); |
346 | 368 | } |
347 | | - |
| 369 | +
|
348 | 370 | .error { |
349 | 371 | border-left: 5px solid var(--error-425-625); |
350 | 372 | padding: 1rem; |
351 | 373 | margin-bottom: 1rem; |
352 | 374 | background-color: var(--error-950-100); |
353 | 375 | } |
354 | | - |
| 376 | +
|
355 | 377 | .warning { |
356 | 378 | border-left: 5px solid var(--warning-425-625); |
357 | 379 | padding: 1rem; |
358 | 380 | margin-bottom: 1rem; |
359 | 381 | background-color: var(--warning-950-100); |
360 | 382 | } |
361 | | - |
| 383 | +
|
362 | 384 | .debug { |
363 | 385 | margin-top: 1rem; |
364 | 386 | padding: 1rem; |
|
369 | 391 | display: none; |
370 | 392 | font-size: 0.85rem; |
371 | 393 | } |
372 | | - |
| 394 | +
|
373 | 395 | .htmx-indicator { |
374 | 396 | opacity: 0; |
375 | 397 | transition: opacity 200ms ease-in; |
376 | 398 | } |
377 | | - |
| 399 | +
|
378 | 400 | .htmx-request .htmx-indicator { |
379 | 401 | opacity: 1; |
380 | 402 | } |
381 | | - |
| 403 | +
|
382 | 404 | .htmx-request.htmx-indicator { |
383 | 405 | opacity: 1; |
384 | 406 | } |
385 | | - |
| 407 | +
|
386 | 408 | /* Honeypot field - hidden from users but visible to bots */ |
387 | 409 | .visually-hidden { |
388 | 410 | position: absolute; |
|
391 | 413 | width: 1px; |
392 | 414 | overflow: hidden; |
393 | 415 | } |
394 | | - |
| 416 | +
|
395 | 417 | .fr-subscribe-form { |
396 | 418 | padding: 2rem 0; |
397 | 419 | } |
|
434 | 456 | <div class=\"fr-card__content\"> |
435 | 457 | <h2>%s</h2> |
436 | 458 | <p>%s</p> |
437 | | - |
438 | | - <form hx-post=\"/subscribe\" hx-target=\"#result\" hx-swap=\"outerHTML\" hx-indicator=\"#loading\" class=\"fr-form\"> |
| 459 | +
|
| 460 | + <form hx-post=\"%s/subscribe\" hx-target=\"#result\" hx-swap=\"outerHTML\" hx-indicator=\"#loading\" class=\"fr-form\"> |
439 | 461 | <div class=\"fr-input-group\"> |
440 | 462 | <label class=\"fr-label\" for=\"email\">E-mail</label> |
441 | 463 | <input class=\"fr-input\" type=\"email\" id=\"email\" name=\"email\" placeholder=\"%s\" required> |
|
462 | 484 | </div> |
463 | 485 | </div> |
464 | 486 | </article> |
465 | | - |
| 487 | +
|
466 | 488 | <div id=\"result\"></div> |
467 | 489 | </div> |
468 | 490 | </div> |
|
514 | 536 | </div> |
515 | 537 | </div> |
516 | 538 | </footer> |
517 | | - |
| 539 | +
|
518 | 540 | <!-- DSFR JavaScript --> |
519 | 541 | <script src=\"https://cdn.jsdelivr.net/npm/@gouvfr/dsfr@1.11.0/dist/dsfr/dsfr.module.min.js\" type=\"module\"></script> |
520 | 542 | <script src=\"https://cdn.jsdelivr.net/npm/@gouvfr/dsfr@1.11.0/dist/dsfr/dsfr.nomodule.min.js\" nomodule></script> |
|
525 | 547 | (:title (:page strings)) |
526 | 548 | (:heading (:page strings)) |
527 | 549 | (:subheading (:page strings)) |
| 550 | + base-path ;; Add base path to form action |
528 | 551 | (:email-placeholder (:form strings)) |
529 | 552 | csrf-token |
530 | 553 | (:website-label (:form strings)) |
|
544 | 567 | </div> |
545 | 568 | " (case type |
546 | 569 | "success" "success" |
547 | | - "error" "error" |
| 570 | + "error" "error" |
548 | 571 | "warning" "warning" |
549 | 572 | "info") |
550 | | - heading |
551 | | - (if (seq args) |
552 | | - (apply format message (map escape-html args)) |
553 | | - message)))) |
| 573 | + heading |
| 574 | + (if (seq args) |
| 575 | + (apply format message (map escape-html args)) |
| 576 | + message)))) |
554 | 577 |
|
555 | 578 | (defn debug-result-template [strings type heading-key message & debug-info] |
556 | 579 | (format " |
|
691 | 714 | csrf-token (generate-csrf-token)] |
692 | 715 | {:status 200 |
693 | 716 | :headers {"Content-Type" "text/html" |
694 | | - "Set-Cookie" (format "csrf_token=%s; Path=/; HttpOnly; SameSite=Strict" csrf-token)} |
| 717 | + "Set-Cookie" (format "csrf_token=%s; Path=%s; HttpOnly; SameSite=Strict" |
| 718 | + csrf-token |
| 719 | + (if (str/blank? base-path) "/" base-path))} |
695 | 720 | :body (build-index-html strings lang csrf-token)})) |
696 | 721 |
|
697 | 722 | (defn parse-form-data [request] |
|
893 | 918 | (let [lang (determine-language req) |
894 | 919 | debug-info {:env {:mailgun-list-id mailgun-list-id |
895 | 920 | :mailgun-api-endpoint mailgun-api-endpoint |
896 | | - :mailgun-api-key "****"} |
| 921 | + :mailgun-api-key "****" |
| 922 | + :base-path base-path} |
897 | 923 | :i18n {:current-language (name lang) |
898 | 924 | :available-languages (keys ui-strings) |
899 | 925 | :browser-language (get-in req [:headers "accept-language"])} |
|
907 | 933 | :headers {"Content-Type" "application/json"} |
908 | 934 | :body (json/generate-string debug-info {:pretty true})})) |
909 | 935 |
|
| 936 | +;; Function to normalize URI for path matching |
| 937 | +(defn normalize-uri [uri] |
| 938 | + (let [uri-without-base (if (and (not (str/blank? base-path)) |
| 939 | + (str/starts-with? uri base-path)) |
| 940 | + (let [path (subs uri (count base-path))] |
| 941 | + (if (str/blank? path) "/" path)) |
| 942 | + uri)] |
| 943 | + (log/debug "Normalized URI from" uri "to" uri-without-base) |
| 944 | + uri-without-base)) |
| 945 | + |
910 | 946 | ;; Main app with routes |
911 | 947 | (defn app [req] |
912 | 948 | (let [uri (:uri req) |
| 949 | + normalized-uri (normalize-uri uri) |
913 | 950 | query-params (parse-query-params uri) |
914 | 951 | req-with-params (assoc req :query-params query-params)] |
915 | 952 | (try |
916 | | - (case [(:request-method req) uri] |
| 953 | + (log/debug "Processing request:" (:request-method req) uri) |
| 954 | + (log/debug "Normalized path:" normalized-uri) |
| 955 | + |
| 956 | + (case [(:request-method req) normalized-uri] |
917 | 957 | [:get "/"] (handle-index req-with-params) |
918 | 958 | [:post "/subscribe"] (handle-subscribe req-with-params) |
919 | 959 | [:get "/debug"] (handle-debug req-with-params) |
|
940 | 980 | (defn start-server [& [port]] |
941 | 981 | (let [port (or port 8080)] |
942 | 982 | (log/info (str "Starting server on http://localhost:" port)) |
| 983 | + (log/info (str "Base path: " (if (str/blank? base-path) "[root]" base-path))) |
943 | 984 | (server/run-server app {:port port}))) |
944 | 985 |
|
945 | 986 | ;; Main entry point |
946 | 987 | (when (= *file* (System/getProperty "babashka.file")) |
947 | 988 | (let [args *command-line-args* |
948 | 989 | ;; Check if first argument is a valid port number |
949 | | - port (if (and (seq args) |
| 990 | + port (if (and (seq args) |
950 | 991 | (try (Integer/parseInt (first args)) true |
951 | 992 | (catch NumberFormatException _ false))) |
952 | 993 | (Integer/parseInt (first args)) |
|
962 | 1003 | mailgun-api-key) |
963 | 1004 | (log/error "Missing environment variable") |
964 | 1005 | (do (log/info (str "Starting server on http://localhost:" port)) |
| 1006 | + (log/info (str "Base path: " (if (str/blank? base-path) "[root]" base-path))) |
965 | 1007 | (server/run-server app {:port port}) |
966 | 1008 | ;; Keep the server running |
967 | 1009 | @(promise))))) |
0 commit comments