phoenix-static-files

Phoenix Static File Serving

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "phoenix-static-files" with this command: npx skills add j-morgan6/elixir-claude-optimization/j-morgan6-elixir-claude-optimization-phoenix-static-files

Phoenix Static File Serving

When to Use

Use when serving uploaded files, assets, or any static content through Phoenix.

Critical Concept

If you reference a path like /uploads/photo.jpg in your app, the directory "uploads" MUST be in static_paths() or the file won't be served!

Configuration Required

  1. Define static_paths/0

lib/my_app_web.ex

def static_paths do ~w(assets fonts images uploads favicon.ico robots.txt) end

Rule: Any directory you serve files from must be listed here.

  1. Verify Plug.Static Configuration

lib/my_app_web/endpoint.ex

plug Plug.Static, at: "/", from: :my_app, gzip: false, only: MyAppWeb.static_paths()

The only: MyAppWeb.static_paths() line ensures only configured directories are served.

Common Patterns

User Uploads

Save uploaded file

dest = Path.join(["priv", "static", "uploads", filename]) File.mkdir_p!(Path.dirname(dest)) File.cp!(source, dest)

Store path in database

path = "/uploads/#{filename}"

MUST add to static_paths

def static_paths, do: ~w(assets uploads favicon.ico)

Generated Content

For dynamically generated images, charts, PDFs

def static_paths, do: ~w(assets uploads generated exports favicon.ico)

Multiple Upload Directories

Different directories for different content types

def static_paths do ~w( assets uploads/images uploads/documents uploads/avatars generated favicon.ico ) end

File Structure

Static files must be in priv/static/ :

my_app/ ├── priv/ │ └── static/ │ ├── assets/ # CSS, JS (from esbuild) │ ├── uploads/ # User uploads │ │ ├── image1.jpg │ │ └── doc.pdf │ ├── generated/ # Generated files │ └── favicon.ico

Serving Files

From Templates

<!-- Image --> <img src="/uploads/photo.jpg" alt="Photo" />

<!-- Document download --> <.link href="/uploads/document.pdf" download>Download</.link>

<!-- Static asset --> <img src={~p"/images/logo.png"} alt="Logo" />

From Controllers

def download(conn, %{"filename" => filename}) do path = Path.join(["priv", "static", "uploads", filename])

if File.exists?(path) do send_download(conn, {:file, path}, filename: filename) else conn |> put_status(:not_found) |> text("File not found") end end

Troubleshooting

Files Return 404

Problem: Accessing /uploads/file.jpg returns 404

Fixes:

  • Check static_paths includes "uploads"

  • Verify file exists in priv/static/uploads/

  • Restart server after changing static_paths

  • Check file permissions (should be readable)

Debug helper

def check_static_file(path) do full_path = Path.join(["priv", "static", path])

cond do not File.exists?(full_path) -> "File does not exist: #{full_path}"

not File.readable?(full_path) ->
  "File exists but not readable: #{full_path}"

true ->
  "File OK: #{full_path}"

end end

Files Work in Dev but Not Production

Problem: Files serve correctly locally but fail in production

Fixes:

  • Run mix phx.digest before deployment:

MIX_ENV=prod mix phx.digest

  • Check production endpoint config:

config/runtime.exs

config :my_app, MyAppWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"

  • Ensure files are deployed:

Check your deployment includes priv/static/

Large Files Slow Down Server

Problem: Serving large files (videos, archives) through Phoenix

Solution: Use a CDN or reverse proxy (nginx, CloudFront)

For large files, consider streaming

def download_large(conn, %{"id" => id}) do file = get_file!(id)

conn |> put_resp_header("content-type", file.content_type) |> put_resp_header("content-disposition", ~s(attachment; filename="#{file.name}")) |> send_chunked(200) |> stream_file(file.path) end

defp stream_file(conn, path) do File.stream!(path, [], 2048) |> Enum.reduce_while(conn, fn chunk, conn -> case chunk(conn, chunk) do {:ok, conn} -> {:cont, conn} {:error, :closed} -> {:halt, conn} end end) end

Security Best Practices

  1. Sanitize File Paths

Never use user input directly in file paths:

❌ DANGEROUS - Path traversal attack

def serve_file(conn, %{"path" => user_path}) do send_file(conn, 200, "priv/static/#{user_path}") end

✅ SAFE - Validate and constrain

def serve_file(conn, %{"filename" => filename}) do safe_name = Path.basename(filename) # Remove directory traversal path = Path.join(["priv", "static", "uploads", safe_name])

if File.exists?(path) and String.starts_with?(path, "priv/static/uploads") do send_file(conn, 200, path) else send_resp(conn, 404, "Not found") end end

  1. Content-Type Headers

Set proper content types to prevent XSS:

def serve_image(conn, %{"id" => id}) do image = get_image!(id)

conn |> put_resp_header("content-type", image.content_type) |> put_resp_header("x-content-type-options", "nosniff") |> send_file(200, image.path) end

  1. Access Control

Protect sensitive files:

def download_private(conn, %{"id" => id}) do user = conn.assigns.current_user file = get_file!(id)

if authorized?(user, file) do send_file(conn, 200, file.path) else send_resp(conn, 403, "Forbidden") end end

CDN Integration

For production, serve static files via CDN:

config/runtime.exs

config :my_app, MyAppWeb.Endpoint, static_url: [host: "cdn.example.com", port: 443, scheme: "https"]

Now ~p"/uploads/file.jpg" generates:

https://cdn.example.com/uploads/file.jpg

Cache Control

Set appropriate cache headers:

In endpoint.ex

plug Plug.Static, at: "/", from: :my_app, only: MyAppWeb.static_paths(), cache_control_for_etags: "public, max-age=86400", headers: %{"cache-control" => "public, max-age=31536000"}

Development vs Production

Development - serve files directly

config/dev.exs

config :my_app, MyAppWeb.Endpoint, debug_errors: true, code_reloader: true, check_origin: false, watchers: [...]

Production - optimize serving

config/prod.exs

config :my_app, MyAppWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json", server: true

Quick Reference

1. Add directory to static_paths

def static_paths, do: ~w(assets uploads favicon.ico)

2. Create directory structure

priv/static/uploads/

3. Save files there

Path.join(["priv", "static", "uploads", filename])

4. Reference in templates

<img src="/uploads/#{filename}" />

5. Restart server to apply changes

mix phx.server

Common Mistakes

❌ Forgetting to add directory to static_paths ❌ Not creating the physical directory ❌ Using relative paths in templates ❌ Not restarting server after config change ❌ Trusting user-provided paths ❌ Serving files from outside priv/static/

✅ Always add directories to static_paths ✅ Create directories with File.mkdir_p! ✅ Use absolute paths like "/uploads/file" ✅ Restart after static_paths changes ✅ Validate and sanitize file paths ✅ Keep all static files in priv/static/

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

elixir-essentials

No summary provided by upstream source.

Repository SourceNeeds Review
General

phoenix-liveview

No summary provided by upstream source.

Repository SourceNeeds Review
General

elixir-patterns

No summary provided by upstream source.

Repository SourceNeeds Review