-module(jchat_http_static). -export([init/2]). init(Req0, State) -> Method = cowboy_req:method(Req0), handle_request(Method, Req0, State). handle_request(<<"GET">>, Req0, State) -> Path = cowboy_req:path(Req0), serve_static_file(Path, Req0, State); handle_request(<<"HEAD">>, Req0, State) -> Path = cowboy_req:path(Req0), serve_static_file(Path, Req0, State); handle_request(<<"OPTIONS">>, Req0, State) -> Req1 = cowboy_req:reply(200, #{ <<"access-control-allow-origin">> => <<"*">>, <<"access-control-allow-methods">> => <<"GET, HEAD, OPTIONS">>, <<"access-control-allow-headers">> => <<"content-type">> }, <<>>, Req0), {ok, Req1, State}; handle_request(_Method, Req0, State) -> Req1 = cowboy_req:reply(405, #{ <<"content-type">> => <<"text/plain">> }, <<"Method Not Allowed">>, Req0), {ok, Req1, State}. serve_static_file(Path, Req0, State) -> StaticDir = jchat_config:static_files_dir(), % Convert path to filename Filename = case Path of <<"/">> -> "index.html"; <<"/", Rest/binary>> -> binary_to_list(Rest); _ -> binary_to_list(Path) end, FilePath = filename:join(StaticDir, Filename), case file:read_file(FilePath) of {ok, Content} -> ContentType = get_content_type(Filename), Req1 = cowboy_req:reply(200, #{ <<"content-type">> => ContentType, <<"access-control-allow-origin">> => <<"*">>, <<"cache-control">> => <<"public, max-age=3600">> }, Content, Req0), {ok, Req1, State}; {error, enoent} -> % If file not found and it's not an API request, serve index.html for SPA routing case is_spa_route(Filename) of true -> IndexPath = filename:join(StaticDir, "index.html"), case file:read_file(IndexPath) of {ok, Content} -> Req1 = cowboy_req:reply(200, #{ <<"content-type">> => <<"text/html; charset=utf-8">>, <<"access-control-allow-origin">> => <<"*">> }, Content, Req0), {ok, Req1, State}; {error, _} -> serve_404(Req0, State) end; false -> serve_404(Req0, State) end; {error, _} -> serve_500(Req0, State) end. serve_404(Req0, State) -> Req1 = cowboy_req:reply(404, #{ <<"content-type">> => <<"text/plain">> }, <<"Not Found">>, Req0), {ok, Req1, State}. serve_500(Req0, State) -> Req1 = cowboy_req:reply(500, #{ <<"content-type">> => <<"text/plain">> }, <<"Internal Server Error">>, Req0), {ok, Req1, State}. get_content_type(Filename) -> case filename:extension(Filename) of ".html" -> <<"text/html; charset=utf-8">>; ".css" -> <<"text/css; charset=utf-8">>; ".js" -> <<"application/javascript; charset=utf-8">>; ".json" -> <<"application/json; charset=utf-8">>; ".png" -> <<"image/png">>; ".jpg" -> <<"image/jpeg">>; ".jpeg" -> <<"image/jpeg">>; ".gif" -> <<"image/gif">>; ".svg" -> <<"image/svg+xml">>; ".ico" -> <<"image/x-icon">>; ".woff" -> <<"font/woff">>; ".woff2" -> <<"font/woff2">>; ".ttf" -> <<"font/ttf">>; ".eot" -> <<"application/vnd.ms-fontobject">>; _ -> <<"application/octet-stream">> end. is_spa_route(Filename) -> % Don't serve index.html for actual file extensions case filename:extension(Filename) of "" -> true; % No extension, likely a SPA route _ -> false % Has extension, likely a real file end.