<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
      xml:lang="en-us">
  <title>Josh Thomas</title>
  <subtitle>Latest entries posted to Josh Thomas's blog.</subtitle>
  <link href="https://joshthomas.dev/feeds/blog.xml"
        rel="self"
        type="application/atom+xml" />
  <link href="https://joshthomas.dev/blog/"
        rel="alternate"
        type="text/html" />
  <author>
      <email>josh@joshthomas.dev</email>
      <name>Josh Thomas</name>
      <uri>https://joshthomas.dev/</uri>
  </author>
    <updated>2025-08-30T00:00:00Z</updated>
  <id>https://joshthomas.dev/blog/</id>
    <entry>
      <title>Open Source is a Gift</title>
      <link href="https://joshthomas.dev/blog/2025/open-source-is-a-gift/" />
      <updated>2025-08-30T00:00:00Z</updated>
      <id>https://joshthomas.dev/blog/2025/open-source-is-a-gift/</id>
      <content type="html">
        <![CDATA[<p>I'm not sure of the origin of the phrase, but a quick <a href="https://kagi.com/search?q=open+source+is+a+gift+blog">search</a> suggests <a href="https://www.redotheweb.com/2011/11/13/open-source-is-a-gift.html">this post</a> from 2011 by François Zaninotto as one possible origin.</p>
<p>When people say &quot;open source is a gift,&quot; they usually mean the final product: a library or application given freely to society, with no expectation of reciprocity.</p>
<p>I agree with this take on the phrase. One of the downsides of releasing software under an open source license is the expectations it can set up for some subset of end users: since they got it for free, they assume that support, bug fixes, and feature requests are also owed to them for free. So I like the phrase as a way to push back on those expectations.</p>
<p>However, I want to talk about a slightly different interpretation of the phrase.</p>
<h3 id="the-gift-of-development-in-the-open" tabindex="-1">The Gift of Development in the Open</h3>
<p><a href="https://astral.sh/">Astral</a> is a venture-capital backed company that has written a handful of tools for the Python ecosystem: <a href="https://astral.sh/ruff">ruff</a> for extremely fast formatting and linting, <a href="https://github.com/astral-sh/uv">uv</a> for packaging and dependency management, <a href="https://github.com/astral-sh/ty">ty</a> for type checking, and most recently a package manager, <a href="https://astral.sh/pyx">pyx</a>.</p>
<p>All of these (except pyx) are MIT licensed and developed completely in the open.</p>
<p>Astral's tools have generally been well received. In my experience, this comes down to taste in API design. Their tools just feel good to use. Still, people have (understandably) raised concerns about the Python ecosystem coalescing around tooling from a VC-backed company, given the general likelihood of <a href="https://pluralistic.net/2023/01/08/watch-the-surpluses/#exogenous-shocks">enshittification</a> that VC money tends to lead to.</p>
<p>I'm not here to talk about that entirely valid concern. Instead, I want to talk about what a gift it is that the development of these well-designed, well-engineered tools is happening entirely in public. Seriously: go look at the pull requests in any of their repos and you will see extremely smart programmers tackling thorny issues in real time. The fact that anyone can learn from that is itself a gift.</p>
<h3 id="the-gift-of-learning-in-public" tabindex="-1">The Gift of Learning in Public</h3>
<p>For about a year now, I've been working on a <a href="https://github.com/joshuadavidthomas/django-language-server">language server for Django</a> as a side project. I chose to write it in Rust, despite never having used a low-level, statically typed systems language before.</p>
<p>From one perspective, this was a mistake. If I were being paid to deliver it for a company, I'd agree. The cross-language barrier adds real friction (even with tools like <a href="https://github.com/PyO3/maturin">maturin</a> and <a href="https://github.com/PyO3/pyo3">PyO3</a> smoothing the way). A well-supported <a href="https://github.com/openlawlibrary/pygls">language server library</a> already exists in Python, and there's even a <a href="https://github.com/fourdigits/django-template-lsp">Django language server</a> written in Python that is far more full-featured than mine. Had I chosen Python, I'd no doubt be further along and closer to matching those features.</p>
<p>But I'm not getting paid for this. This is a personal project.</p>
<p>So I chose Rust for entirely personal reasons: I wanted to learn it in a meaningful, non-toy context. Rust offers performance essentially for free, and its type system and borrow checker, while painful at first, force you to think differently about correctness and memory safety. After years of wrestling with type checking in Python projects using <a href="https://github.com/python/mypy">mypy</a> and <a href="https://github.com/microsoft/pyright">pyright</a>, working in a language with <em>actual</em> types has been refreshing.</p>
<p>And honestly: it was fun.</p>
<p>This is where the &quot;gift&quot; of open source really hit me. Astral's codebases and PRs have become my textbook. Their discussions around error handling, incremental parsing, cross-platform quirks, performance trade-offs - all of it has been incredibly useful while I've been learning Rust.</p>
<p>And beyond reading, I borrowed ideas and techniques directly. The architecture of my Django language server is heavily influenced by techniques I first saw in their repos: using <a href="https://github.com/salsa-rs/salsa">salsa</a> for incremental cached computations, structuring a Rust project into multiple crates within a workspace, and even approaches for integrating cleanly with Python. Astral, in turn, built on ideas from other open projects like <a href="https://github.com/rust-lang/rust-analyzer">rust-analyzer</a> and <a href="https://github.com/rust-lang/cargo">Cargo</a> itself.</p>
<p>That chain of influence is what makes open source special.</p>
<h3 id="the-gift-passed-along" tabindex="-1">The Gift Passed Along</h3>
<p>So yeah, the original meaning of &quot;open source is a gift&quot; holds up. But I think there's more to it than the code you <code>uv add</code> or <code>pip install</code> or <code>cargo add</code>. There's the act of doing the work in the open. Bug triages, design debates in GitHub threads, pull requests anyone can read through - it's all basically a free, ongoing seminar in how good software gets made.</p>
<p>Astral, of course, isn't the only one building in the open - they're just the most relevant to my journey this past year. Countless others, from <a href="https://github.com/python/cpython">CPython</a> to <a href="https://github.com/django/django">Django</a> to projects maintained entirely by volunteers, are out there too, all ready for anyone to study.</p>
<p>The software you install is the obvious gift. But I think there's something just as valuable in the process being open too. It gives you permission to watch how good software gets made, borrow what works, and eventually put your own stuff out there for someone else to learn from.</p>
]]>
      </content>
    </entry>
    <entry>
      <title>Debugging a "No time zone found" error while using the official Playwright Docker image</title>
      <link href="https://joshthomas.dev/blog/2024/debugging-a-no-time-zone-found-error-while-using-the-official-playwright-docker-image/" />
      <updated>2024-05-01T18:48:00Z</updated>
      <id>https://joshthomas.dev/blog/2024/debugging-a-no-time-zone-found-error-while-using-the-official-playwright-docker-image/</id>
      <content type="html">
        <![CDATA[<p>I recently ran into an odd error after refactoring a small part of the <a href="https://github.com/westerveltco/django-twc-project/blob/af641ecb727e9b3e6efae420b1c1101cffc3fbdc/examples/default/Dockerfile"><code>Dockerfile</code> template</a> we use at my <a href="https://westervelt.com/">day job</a>. It took me a while to chase down where the error was coming from and connect all the dots to realize that the change I had made was the reason for that error.</p>
<p>After making the change to the <code>Dockerfile</code>, I went and worked on something else unrelated for a while. When I circled back after some time had passed and the change had left my brain, I built and started the Docker Compose stack for the application using the updated <code>Dockerfile</code>. That's when the application threw the following traceback:</p>
<pre class="language-bash"><i class="devicon-bash-plain code-icon"></i><code class="language-bash">  File <span class="token string">"/usr/local/lib/python3.12/site-packages/django/utils/timezone.py"</span>, line <span class="token number">52</span>, <span class="token keyword">in</span> get_default_timezone
    <span class="token builtin class-name">return</span> zoneinfo.ZoneInfo<span class="token punctuation">(</span>settings.TIME_ZONE<span class="token punctuation">)</span>
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File <span class="token string">"/usr/local/lib/python3.12/zoneinfo/_common.py"</span>, line <span class="token number">24</span>, <span class="token keyword">in</span> load_tzdata
    raise ZoneInfoNotFoundError<span class="token punctuation">(</span>f<span class="token string">"No time zone found with key {key}"</span><span class="token punctuation">)</span>
zoneinfo._common.ZoneInfoNotFoundError: <span class="token string">'No time zone found with key America/Chicago'</span></code></pre>
<p>I thought that was odd since I had never encountered this error before. I've created plenty of timezone related bugs, but the data had always been there for me to screw up. So, what had changed? Where did my timezones go?</p>
<h3 id="an-unrelated-change%3F" tabindex="-1">An unrelated change?</h3>
<p>As part of our common <code>Dockerfile</code> template, we install Playwright for end-to-end testing. It's installed in two stages: one to install the library and all the system dependencies it needs, and another to use that stage to load our application code in development. It looks like this:</p>
<pre class="language-dockerfile"><i class="devicon-docker-plain code-icon"></i><code class="language-dockerfile"><span class="token comment"># the `py` stage (not shown) installs all of our application's</span>
<span class="token comment"># Python dependencies</span>
<span class="token instruction"><span class="token keyword">FROM</span> py <span class="token keyword">as</span> playwright</span>
<span class="token instruction"><span class="token keyword">ENV</span> PLAYWRIGHT_BROWSERS_PATH /usr/local/bin/playwright-browsers</span>
<span class="token instruction"><span class="token keyword">RUN</span> <span class="token options"><span class="token property">--mount</span><span class="token punctuation">=</span><span class="token string">type=cache,target=/var/cache/apt,sharing=locked</span> <span class="token operator">\</span>
  <span class="token property">--mount</span><span class="token punctuation">=</span><span class="token string">type=cache,target=/var/lib/apt,sharing=locked</span></span> <span class="token operator">\</span>
  apt-get update --fix-missing <span class="token operator">\</span>
  &amp;&amp; playwright install --with-deps</span>

<span class="token instruction"><span class="token keyword">FROM</span> playwright <span class="token keyword">as</span> dev</span>
<span class="token comment"># the `app` stage (not shown) copies all of our application's source code</span>
<span class="token comment"># into the Docker image</span>
<span class="token instruction"><span class="token keyword">COPY</span> <span class="token options"><span class="token property">--from</span><span class="token punctuation">=</span><span class="token string">app</span></span> --link /app /app</span></code></pre>
<p>While this worked, whenever the cache for the initial <code>playwright</code> build stage was invalidated -- typically by changing something regarding a Python dependency -- you would have to install Playwright, its system dependencies, and the browsers all over again. I started to get very frustrated with this as the build process for Playwright involves installing a ton of system dependencies and the browsers themselves, so it can be quite slow.</p>
<p>As a solution, I decided to leverage the Docker image Microsoft provides at <code>mcr.microsoft.com/playwright/python</code> instead of installing Playwright manually. As a bonus, it got rid of the extra build stage and slimmed our <code>Dockerfile</code> down slightly.</p>
<pre class="language-diff"><i class="devicon-git-plain code-icon"></i><code class="language-diff"><span class="token deleted-sign deleted"><span class="token prefix deleted">-</span><span class="token line">FROM py as playwright
</span><span class="token prefix deleted">-</span><span class="token line">ENV PLAYWRIGHT_BROWSERS_PATH /usr/local/bin/playwright-browsers
</span><span class="token prefix deleted">-</span><span class="token line">RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
</span><span class="token prefix deleted">-</span><span class="token line">  --mount=type=cache,target=/var/lib/apt,sharing=locked \
</span><span class="token prefix deleted">-</span><span class="token line">  apt-get update --fix-missing \
</span><span class="token prefix deleted">-</span><span class="token line">  &amp;&amp; playwright install --with-deps
</span></span>
<span class="token deleted-sign deleted"><span class="token prefix deleted">-</span><span class="token line">FROM playwright as dev
</span></span><span class="token inserted-sign inserted"><span class="token prefix inserted">+</span><span class="token line">FROM mcr.microsoft.com/playwright/python:v1.43.0 as dev
</span><span class="token prefix inserted">+</span><span class="token line">COPY --from=py --link /usr/local /usr/local
</span></span><span class="token unchanged"><span class="token prefix unchanged"> </span><span class="token line">COPY --from=app --link /app /app
</span></span></code></pre>
<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><i class="fa-solid fa-circle-info octicon" height="16" width="16" aria-hidden="true"></i>Note</p><p>Microsoft does not provide a tag for major or minor versions, only the full version number including the patch, so to ensure there is no drift between the version of Playwright used in the <code>Dockerfile</code> stage and the version installed by the application, I also adjusted the <code>requirements.in</code> to pin the version there as well:</p>
<pre class="language-linuxconfig"><i class="devicon-linuxconfig-plain code-icon"></i><code class="language-linuxconfig">playwright==1.43.0</code></pre>
</div>
<p>Once I made this change, I went and worked on something else. When I came back, I saw the timezone error at the top of the post and was very confused. Where did the timezone data go? Did I bump the version of Python or Django without realizing it and there was a breaking change there? It was just working this morning, why would it not work now? Nothing has changed, I haven't even touched anything related to timezones!</p>
<h3 id="connecting-the-dots" tabindex="-1">Connecting the dots</h3>
<p>It had been just long enough since making the Playwright change that I had completely forgotten that the call was coming from inside the house.</p>
<p>It took me a while to connect the dots, but after testing a bunch of different things -- downgrading both Python and Django, forcing an upgrade to the latest for both, combing through my <code>Dockerfile</code> line-by-line for any clue -- I managed to track down the source of the bug after looking at the source code for the two <code>Dockerfile</code> images.</p>
<p>Playwright Docker image's base is <code>ubuntu</code> and does not come with <code>tzdata</code> installed in the base image -- which Python's stdlib library <code>zoneinfo</code> relies on for Linux. Previously, I used the official Python Docker image as a base for all build stages, which does install <code>tzdata</code>.</p>
<p>Once I tracked this down, it was a simple fix:</p>
<pre class="language-diff"><i class="devicon-git-plain code-icon"></i><code class="language-diff"><span class="token unchanged"><span class="token prefix unchanged"> </span><span class="token line">FROM playwright as dev
</span><span class="token prefix unchanged"> </span><span class="token line">FROM mcr.microsoft.com/playwright/python:v1.43.0 as dev
</span><span class="token prefix unchanged"> </span><span class="token line">COPY --from=py --link /usr/local /usr/local
</span><span class="token prefix unchanged"> </span><span class="token line">COPY --from=app --link /app /app
</span></span><span class="token inserted-sign inserted"><span class="token prefix inserted">+</span><span class="token line">RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
</span><span class="token prefix inserted">+</span><span class="token line">  --mount=type=cache,target=/var/lib/apt,sharing=locked \
</span><span class="token prefix inserted">+</span><span class="token line">  apt-get update --fix-missing \
</span><span class="token prefix inserted">+</span><span class="token line">  &amp;&amp; apt-get install -y --no-install-recommends \
</span><span class="token prefix inserted">+</span><span class="token line">  tzdata \
</span><span class="token prefix inserted">+</span><span class="token line">  &amp;&amp; apt-get autoremove -y &amp;&amp; apt-get clean -y &amp;&amp; rm -rf /var/lib/apt/lists/*
</span></span></code></pre>
<p>I considered adding the <code>tzdata</code> Python package to my application's dependencies. However, since this Docker build stage is solely used in development and I can rely on the official Python Docker image to include <code>tzdata</code> (for now!), sticking with this approach seemed the best way forward.</p>
<hr />
<p>This was an interesting bug to track down. Fortunately, the recent switch to the Playwright Docker image was still fresh in my mind. Even though I initially forgot about the change, this proximity in time helped me quickly track down the source of the bug. Had there been a delay of a day or more between introducing the change and the bug happening, I can't help but wonder how much longer it might have taken to solve this puzzle.</p>
]]>
      </content>
    </entry>
    <entry>
      <title>How I organize `staticfiles` in my Django projects</title>
      <link href="https://joshthomas.dev/blog/2024/how-i-organize-staticfiles-in-my-django-projects/" />
      <updated>2024-04-20T20:26:00Z</updated>
      <id>https://joshthomas.dev/blog/2024/how-i-organize-staticfiles-in-my-django-projects/</id>
      <content type="html">
        <![CDATA[<p>In the most recent meeting of Jeff Triplett's <a href="https://micro.webology.dev/2024/04/12/office-hours-on.html">office hours</a>, the topic of how everyone handled the static CSS and JS files in their Django projects came up. I mentioned my preferred setup, which Jeff himself is familiar with as at my <a href="https://westervelt.com/">day job</a> we have contracted his Django consultancy <a href="https://revsys.com/">REVSYS</a> to help out with our development needs, but rather than just keeping it contained to that Zoom meeting I thought it might be useful to write about it.</p>
<p>So, here's how I like to organize the <code>staticfiles</code> in my Django projects and the reasoning behind the choices I've made.</p>
<p>For context, these days I mostly build Django projects with server-side generated templates. Standard, predictable... the <a href="https://boringtechnology.club/">boring</a> choice. Within those Django templates, I use the following:</p>
<ul>
<li>Tailwind CSS with its utility CSS classes for styling, using the excellent <a href="https://github.com/oliverandrich/django-tailwind-cli"><code>django-tailwind-cli</code></a></li>
<li>HTMX for getting SPA-like interactions</li>
<li>Alpine.js for sprinkles of JS interactivity (menu dropdowns, dynamic CSS classes, etc.)</li>
</ul>
<p>For projects requiring more than what the HTMX and Alpine.js combination offers, I use React and Vite, with <a href="https://github.com/MrBin99/django-vite"><code>django-vite</code></a> helping integrate them with Django.</p>
<p>Below is the tree view layout of the relevant <code>staticfiles</code> directories in my projects:</p>
<pre class="language-shell"><i class="devicon-bash-plain code-icon"></i><code class="language-shell">project/
├── static/
│   ├── dist/
│   ├── public/
│   └── src/
└── staticfiles/</code></pre>
<p>We'll start with <code>static/public/</code> and <code>static/src/</code>, as those are where the actual files go.</p>
<p><code>static/public/</code> is for assets that require no processing by external tools, such as logo images, vendored CSS/JS assets, and any handwritten vanilla JS files that do not need to be compiled or transpiled.</p>
<p><code>static/src/</code> houses assets that require processing to be usable. For instance, my Tailwind CSS <code>styles.css</code> file and any JavaScript/TypeScript files destined for Vite processing are placed here.</p>
<p>After those assets have been run through whatever build pipeline they need, they end up in the <code>static/dist/</code> folder.</p>
<p><code>staticfiles/</code> serves as the collection point for all assets, gathered via the <code>python manage.py collectstatic</code> Django management command during the deployment build process.</p>
<p>Here are the Django settings you need to set in order to use this layout:</p>
<pre class="language-python"><i class="devicon-python-plain code-icon"></i><code class="language-python"><span class="token comment"># settings.py</span>
<span class="token comment"># django.contrib.staticfiles</span>
STATIC_ROOT <span class="token operator">=</span> BASE_DIR <span class="token operator">/</span> <span class="token string">"staticfiles"</span>

STATIC_URL <span class="token operator">=</span> <span class="token string">"/static/"</span>

STATICFILES_DIRS <span class="token operator">=</span> <span class="token punctuation">[</span>
    BASE_DIR <span class="token operator">/</span> <span class="token string">"static"</span> <span class="token operator">/</span> <span class="token string">"dist"</span><span class="token punctuation">,</span>
    BASE_DIR <span class="token operator">/</span> <span class="token string">"static"</span> <span class="token operator">/</span> <span class="token string">"public"</span><span class="token punctuation">,</span>
<span class="token punctuation">]</span>

<span class="token comment"># django-tailwind-cli</span>
TAILWIND_CLI_DIST_CSS <span class="token operator">=</span> <span class="token string">"css/tailwind.css"</span>

TAILWIND_CLI_SRC_CSS <span class="token operator">=</span> <span class="token string">"static/src/tailwind.css"</span>

<span class="token comment"># django-vite (optional)</span>
DJANGO_VITE_ASSETS_PATH <span class="token operator">=</span> BASE_DIR <span class="token operator">/</span> <span class="token string">"static"</span> <span class="token operator">/</span> <span class="token string">"dist"</span></code></pre>
<p>With this configuration, the <code>collectstatic</code> command will gather all files from <code>STATICFILES_DIRS</code> (<code>static/dist/</code> and <code>static/public/</code> here in these example settings), store them in <code>STATIC_ROOT</code> (<code>staticfiles/</code>), and in production serve them under the url prefix <code>STATIC_URL</code> (<code>/static/</code>).</p>
<p>A quirk of the <code>django-tailwind-cli</code> configuration is that the path for the source CSS file originates from the <code>BASE_DIR</code>, whereas the path for the compiled CSS file is relative to the first location listed in <code>STATICFILES_DIRS</code>. That means the <code>TAILWIND_CLI_SRC_CSS</code> setting <strong>must</strong> include the <code>static/src/</code> prefix, while <code>TAILWIND_CLI_DIST_CSS</code> should omit the <code>static/dist/</code> prefix to align with the expectations of the package. Importantly, <code>django-tailwind-cli</code> considers only the first entry in <code>STATICFILES_DIRS</code> for locating compiled assets, which is why <code>static/dist/</code> is positioned first in the list.</p>
<p>For completeness, below is an example <code>vite.config.ts</code> file for projects that process JavaScript/TypeScript files using Vite.</p>
<pre class="language-typescript"><i class="devicon-typescript-plain code-icon"></i><code class="language-typescript"><span class="token comment">// vite.config.ts</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> defineConfig <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"vite"</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> resolve <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"path"</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> react <span class="token keyword">from</span> <span class="token string">"@vitejs/plugin-react"</span><span class="token punctuation">;</span>

<span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token function">defineConfig</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
    <span class="token comment">// Set the base path for all static assets. Useful for deployment where paths need prefixing.</span>
    base<span class="token operator">:</span> <span class="token string">"/static/"</span><span class="token punctuation">,</span>
    build<span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token comment">// Directory for storing build-time assets (empty here to avoid nesting).</span>
        assetsDir<span class="token operator">:</span> <span class="token string">""</span><span class="token punctuation">,</span>
        <span class="token comment">// Enable the generation of manifest.json for asset management.</span>
        manifest<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
        <span class="token comment">// Output directory for built files, resolved to 'static/dist' within the project.</span>
        outDir<span class="token operator">:</span> <span class="token function">resolve</span><span class="token punctuation">(</span>__dirname<span class="token punctuation">,</span> <span class="token string">"./static/dist"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        rollupOptions<span class="token operator">:</span> <span class="token punctuation">{</span>
            <span class="token comment">// Entry point for the app, necessary for multi-page apps to specify multiple entries.</span>
            input<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token function">resolve</span><span class="token punctuation">(</span>__dirname<span class="token punctuation">,</span> <span class="token string">"./static/src/main.tsx"</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
            output<span class="token operator">:</span> <span class="token punctuation">{</span>
                chunkFileNames<span class="token operator">:</span> <span class="token keyword">undefined</span><span class="token punctuation">,</span>
            <span class="token punctuation">}</span><span class="token punctuation">,</span>
        <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    plugins<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token function">react</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token comment">// The directory to serve as the public folder (empty here since we're managing paths manually).</span>
    publicDir<span class="token operator">:</span> <span class="token string">""</span><span class="token punctuation">,</span>
    resolve<span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token comment">// Aliases to simplify imports; '@' points to the 'static/src' directory.</span>
        alias<span class="token operator">:</span> <span class="token punctuation">{</span>
            <span class="token string-property property">"@"</span><span class="token operator">:</span> <span class="token function">resolve</span><span class="token punctuation">(</span>__dirname<span class="token punctuation">,</span> <span class="token string">"./static/src"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token punctuation">}</span><span class="token punctuation">,</span>
        extensions<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">".js"</span><span class="token punctuation">,</span> <span class="token string">".jsx"</span><span class="token punctuation">,</span> <span class="token string">".ts"</span><span class="token punctuation">,</span> <span class="token string">".tsx"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token comment">// Root directory for source files, set to 'static/src' to centralize code.</span>
    root<span class="token operator">:</span> <span class="token function">resolve</span><span class="token punctuation">(</span>__dirname<span class="token punctuation">,</span> <span class="token string">"./static/src"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    server<span class="token operator">:</span> <span class="token punctuation">{</span>
        host<span class="token operator">:</span> <span class="token string">"127.0.0.1"</span><span class="token punctuation">,</span>
        port<span class="token operator">:</span> <span class="token number">5173</span><span class="token punctuation">,</span>
        open<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
        watch<span class="token operator">:</span> <span class="token punctuation">{</span>
            usePolling<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
            disableGlobbing<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
        <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Additionally, you should update your project's <code>.gitignore</code> to exclude the <code>static/dist/</code> and <code>staticfiles/</code> directories from version control. This prevents unnecessary tracking of compiled and collected assets:</p>
<pre class="language-unixconfig"><i class="devicon-unixconfig-plain code-icon"></i><code class="language-unixconfig">#.gitignore
staticfiles/
static/dist/
# Optionally, to universally ignore all 'dist' directories:
# dist</code></pre>
<h3 id="what-it-looks-like-in-practice" tabindex="-1">What it looks like in practice</h3>
<p>Below is the structure of the <code>static/</code> folder from one of my smaller Django projects.</p>
<p>You can see the <code>tailwind.css</code> file in both the <code>static/src/css/</code> and <code>static/dist/css/</code> folders. The version in <code>src</code> is the shell CSS file required by Tailwind CSS for its initial configuration, while the one in <code>dist</code> contains the compiled CSS styles actively used by the project.</p>
<p>For JavaScript libraries, I use the <code>static/public/vendor/js/</code> folder to store minified scripts for HTMX and Alpine.js. I prefer to place any vendored assets in their own <code>vendor/</code> folder, which helps keep the directory structure clean and organized for easier management.</p>
<pre class="language-shell"><i class="devicon-bash-plain code-icon"></i><code class="language-shell">static/
├── dist/
│   └── css/
│       └── tailwind.css          <span class="token comment"># Compiled CSS</span>
├── public/
│   ├── vendor/
│   │    └── js/
│   │        ├── alpinejs.min.js  <span class="token comment"># Minified Alpine.js</span>
│   │        └── htmx.min.js      <span class="token comment"># Minified HTMX</span>
│   └── logo-sm.png               <span class="token comment"># Static image asset</span>
└── src/
    └── css/
        └── tailwind.css          <span class="token comment"># Source CSS for Tailwind</span></code></pre>
<p>For a slightly more complicated setup, consider a project of mine that incorporates a fairly large React SPA. The structure below demonstrates how I organize files to support both development and production environments effectively:</p>
<pre class="language-shell"><i class="devicon-bash-plain code-icon"></i><code class="language-shell">static/
├── dist/
│   ├── css/
│   │   └── tailwind.css     <span class="token comment"># Compiled CSS</span>
│   ├── main-BvH4oN1P.js     <span class="token comment"># Compiled main JS file with hash for cache busting</span>
│   ├── main-D-2GwwJG.css    <span class="token comment"># Compiled main CSS file with hash</span>
│   ├── router-9A0RiP7h.css  <span class="token comment"># Compiled router CSS with hash</span>
│   └── router-DlIDJo3H.js   <span class="token comment"># Compiled router JS with hash</span>
├── public/
│   └── logo-sm.png          <span class="token comment"># Static image asset</span>
└── src/
    ├── api/                 <span class="token comment"># API interaction layers</span>
    ├── components/          <span class="token comment"># React components</span>
    ├── config/              <span class="token comment"># Configuration files</span>
    ├── contexts/            <span class="token comment"># React contexts</span>
    ├── helpers/             <span class="token comment"># Helper functions</span>
    ├── hooks/               <span class="token comment"># Custom React hooks</span>
    ├── images/              <span class="token comment"># Source images</span>
    ├── models/              <span class="token comment"># Data models</span>
    ├── queries/             <span class="token comment"># Data fetching queries</span>
    ├── routes/              <span class="token comment"># Route definitions</span>
    ├── scss/                <span class="token comment"># SCSS files before processing</span>
    ├── utils/               <span class="token comment"># Utility functions</span>
    ├── main.tsx             <span class="token comment"># Main entry point for the SPA</span>
    ├── router.tsx           <span class="token comment"># Router setup</span>
    ├── tailwind.css         <span class="token comment"># Source CSS for Tailwind</span>
    └── vite-env.d.ts        <span class="token comment"># TypeScript definitions for Vite</span></code></pre>
<p>This directory structure accommodates a comprehensive React application by separating source files, components, and configuration data into subdirectories within <code>src/</code>. The <code>dist/</code> directory holds compiled and versioned assets.</p>
<h3 id="why%3F" tabindex="-1">Why?</h3>
<p>So, why opt for this segmented directory setup rather than a single <code>static/</code> or <code>assets/</code> folder containing all CSS, JS, and other static files directly in the base of the project?</p>
<p>To be honest, this is a strategy that many popular JS frameworks — such as Astro, Next.js, and SvelteKit — get right. They clearly distinguish between source files requiring processing and those that can be directly served. This common pattern, which I've adopted, involves placing directly servable files within a <code>public/</code> directory and others in the <code>src/</code> directory. This approach allows anyone to immediately understand which source files depend on some sort of build pipeline.</p>
<p>One consideration when using two source folders but only one distribution folder is the potential for file conflicts and one folder’s files clobbering another’s. However, this can generally be managed by maintaining unique filenames or organizing files within specific directories. Additionally, when using tools like Vite, which generates hashed filenames during the build, this concern is mitigated. Nonetheless, it's a trade-off worth noting in this setup.</p>
<hr />
<p>Thanks for reading! I hope you've found this post useful — whether it's inspired you to try a new way of organizing your Django staticfiles, or even if it's a method you'd prefer to avoid.</p>
<p>I'm on Mastodon at <a href="https://joshthomas.dev/@josh">@josh@joshthomas.dev</a> and would love to hear what you think.</p>
<p>Many thanks to <a href="https://mastodon.social/@webology">Jeff</a> for reading the first draft and offering his thoughts on how I could improve it.</p>
]]>
      </content>
    </entry>
    <entry>
      <title>New Year, New Website</title>
      <link href="https://joshthomas.dev/blog/2024/new-year-new-website/" />
      <updated>2024-01-13T03:24:00Z</updated>
      <id>https://joshthomas.dev/blog/2024/new-year-new-website/</id>
      <content type="html">
        <![CDATA[<p>It's a new year and I've decided to rebuild my personal website.</p>
<p>Again.</p>
<p>I wanted to write a much longer post about the history of my personal website -- and I definitely will later, unless my ADHD gets the better of me -- but if I don't at least write something, I'll never get this website refresh out the door. For now I'll just say, I've settled on using the language and framework I'm most comfortable with, Python and Django.</p>
<p>This site has been through many iterations built with so many different languages and frameworks over the years. It's been fun to dabble and it's taught me a lot. That's what I like best about having a personal website, being able to try out new things in public but with relatively low stakes.</p>
<p>But these days with a very busy full time job, 2 kids, a wife, a house and yard to take care of, and the desire to have some fun with my free time, I find I just don't have the patience for the upkeep that comes with using something I don't know for this site. I still want this to be a playground for exploring new ideas, but for now I'm choosing the <a href="https://boringtechnology.club/">boring</a> choice and going with what I know.</p>
<p>For those curious, the site is open source and is available over on my <a href="https://github.com/joshuadavidthomas/joshthomas.dev">GitHub</a> profile.</p>
]]>
      </content>
    </entry></feed>