🎯

phoenix-static-files

🎯Skill

from j-morgan6/elixir-claude-optimization

VibeIndex|
What it does

Configures and manages static file serving in Phoenix, ensuring proper directory setup and access for uploads, assets, and generated content.

πŸ“¦

Part of

j-morgan6/elixir-claude-optimization(7 items)

phoenix-static-files

Installation

Add MarketplaceAdd marketplace to Claude Code
/plugin marketplace add j-morgan6/elixir-claude-optimization
Install PluginInstall plugin from marketplace
/plugin install elixir-optimization --scope user
Claude CodeAdd plugin in Claude Code
/plugin list
Install ScriptRun install script
curl -sL https://raw.githubusercontent.com/j-morgan6/elixir-claude-optimization/main/install.sh | bash
git cloneClone repository
git clone https://github.com/j-morgan6/elixir-claude-optimization.git
πŸ“– Extracted from docs: j-morgan6/elixir-claude-optimization
3Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

Use when serving uploaded files, assets, or any static content. Covers static_paths configuration, Plug.Static setup, and troubleshooting file serving issues.

Overview

# 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

```elixir

# 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.

2. Verify Plug.Static Configuration

```elixir

# 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

```elixir

# 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

```elixir

# For dynamically generated images, charts, PDFs

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

```

Multiple Upload Directories

```elixir

# 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

```heex

Photo

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

Logo

```

From Controllers

```elixir

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:

  1. Check static_paths includes "uploads"
  2. Verify file exists in priv/static/uploads/
  3. Restart server after changing static_paths
  4. Check file permissions (should be readable)

```elixir

# 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:

  1. Run mix phx.digest before deployment:

```bash

MIX_ENV=prod mix phx.digest

```

  1. Check production endpoint config:

```elixir

# config/runtime.exs

config :my_app, MyAppWeb.Endpoint,

cache_static_manifest: "priv/static/cache_manifest.json"

```

  1. 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)

```elixir

# 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:

```elixir

# ❌ 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

```

2. Content-Type Headers

Set proper content types to prevent XSS:

```elixir

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

```

3. Access Control

Protect sensitive files:

```elixir

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:

```elixir

# 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:

```elixir

# 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

```elixir

# 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

```elixir

# 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

# 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/