<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Photo stream</title>
    <description>Dries Buytaert on Photo stream.</description>
    <link>https://dri.es/tag/photo-stream</link>
    <atom:link href="https://dri.es/tag/photo-stream/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Infinite scroll with htmx</title>
      <link>https://dri.es/infinite-scroll-with-htmx</link>
      <guid>https://dri.es/infinite-scroll-with-htmx</guid>
      <pubDate>Wed, 26 Nov 2025 17:55:48 -0500</pubDate>
      <description><![CDATA[<p>Several years ago, <a href="https://dri.es/a-photo-stream-for-my-site">I built a photo stream</a> on my Drupal-powered website. You can see it at https://dri.es/photos. This week, I gave it a small upgrade: infinite scroll.</p>
<p>My first implementation used vanilla JavaScript using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">Intersection Observer API</a>, and it worked fine. It took about 30 lines of custom JavaScript and 20 lines of PHP code.</p>
<p>But Drupal now ships with <a href="https://htmx.org/">htmx</a> support, and that had been on my mind. So a couple of hours later, I rewrote the feature with htmx to see if it could do the same job more simply.</p>
<p>It's something I love about <a href="https://www.drupal.org/">Drupal</a>: how we keep adding small, well-chosen features like htmx support. Not flashy, but they quietly make everyday work nicer. Years ago, Drupal was one of the first CMSes to adopt jQuery, and our early adoption helped contribute to its widespread use. Today, we're replacing parts of jQuery with htmx, and Drupal may well be among the first CMSes to ship htmx in core.</p>
<p>If, like me, you haven't used htmx before, it lets you add dynamic behavior to pages using HTML attributes instead of writing JavaScript. Want to load content when something is clicked or scrolled into view? You add an attribute like <code>hx-get=&quot;/load-more&quot;</code> and htmx handles the request, then swaps the response into your page. It gives you <a href="https://en.wikipedia.org/wiki/Ajax_(programming)">AJAX</a>-style interactions without having to write JavaScript.</p>
<p>To make the photo stream load more images as you scroll, I added an &quot;htmx trigger&quot;. When it scrolls into view, htmx fetches more photos and appends them to the right container.  The resulting HTML looks like this:</p>
<pre><code class="language-html">&lt;div hx-get=&quot;/photos/load-more?offset=25&quot;
         hx-trigger=&quot;revealed&quot;
         hx-target=&quot;#album&quot;
         hx-swap=&quot;beforeend&quot;&gt;
  &lt;figure&gt;
   ...
  &lt;/figure&gt;
&lt;/div&gt;
</code></pre>
<p>The <code>hx-get</code> points to a controller that returns the next batch of photos. The <code>hx-trigger=&quot;revealed&quot;</code> attribute means &quot;fire when scrolled into view&quot;. The <code>hx-target=&quot;#album&quot;</code> tells htmx where to put the new content, and <code>hx-swap=&quot;beforeend&quot;</code> appends it at the end of that <code>#album</code> container.</p>
<p>I didn't want users to hit the last photo and have to wait for more to load. To keep the scrolling smooth, I added the trigger a few photos <em>before</em> the end. This pre-fetches the next batch before the user even realizes they are running out of photos. This is what the code in Drupal looks likes:</p>
<pre><code class="language-php">// Trigger 3 images before the end to prefetch the next batch.
$trigger = array_keys($images)[max(0, count($images) - 4)];

foreach ($images as $key =&gt; $image) {
  …

  if ($key === $trigger) {
    // Add htmx attributes to the &lt;div&gt; surrounding the image.
    $build['#attributes']['hx-get'] = '/photos/load-more?offset=' . ($offset + $limit);
    $build['#attributes']['hx-trigger'] = 'revealed';
    $build['#attributes']['hx-target'] = '#album';
    $build['#attributes']['hx-swap'] = 'beforeend';
  }
}
</code></pre>
<p>And the controller that returns the HTML:</p>
<pre><code class="language-php">public function loadMorePhotos(Request $request) {
  $offset = $request-&gt;query-&gt;getInt('offset', 0);
  $limit = 25;
  $photos = PhotoCollection::loadRecent($offset, $limit);
  if (!$photos) {
    return new Response('');
  }

  $build = $this-&gt;buildImages($photos, $offset, $limit);
  $html = \Drupal::service('renderer')-&gt;renderRoot($build);
  return new Response($html);
}
</code></pre>
<p>Each response includes 25 photos. It continues fetching new photos as you scroll down until there are no more photos, at which point the controller returns an empty response and the scrolling stops.</p>
<p>As you can tell, there is <em>no</em> custom JavaScript in my code. It's all abstracted away by htmx. The htmx version took less than 10 lines of PHP code (shown above) instead of 30+ lines of custom JavaScript. The <code>loadMorePhotos</code> controller I needed either way.</p>
<p>The savings are negligible. Replacing a couple dozen lines of JavaScript won't change the world.  And at 16KB gzipped, htmx is much larger than the custom JavaScript I wrote by hand. But it still feels reasonable. My photo stream is image-heavy, and htmx adds less than 0.5% to the initial page weight.</p>
<p>Overall, I'd say that htmx grew on me. There is something satisfying about declarative code. You describe what should happen, and the implementation disappears. I may try it in a few more places to improve the user experience of my site.</p>
]]></description>
    </item>
    <item>
      <title>Life beyond social media: a more intentional way to share photos</title>
      <link>https://dri.es/life-beyond-social-media-a-more-intentional-way-to-share-photos</link>
      <guid>https://dri.es/life-beyond-social-media-a-more-intentional-way-to-share-photos</guid>
      <pubDate>Wed, 08 Oct 2025 06:29:01 -0400</pubDate>
      <description><![CDATA[<p>Several years ago, <a href="https://dri.es/a-photo-stream-for-my-site">I built a photo stream</a> on my website and pretty much stopped posting on Instagram.</p>
<p>I didn't like how Instagram made me feel, or the fact that it tracks my friends and family when they look at my photos. And while it was a nice way to stay in touch with people, I never found a real sense of community there.</p>
<p>Instead, I wanted a space that felt genuinely mine. A place that felt like home, not a podium. No tracking, no popularity contests, no clickbait, no ads. Just a quiet corner to share a bit of my life, where friends and family could visit without being tracked.</p>
<p>Leaving Instagram meant giving up its biggest feature: subscribers and notifications. On Instagram, people follow you and automatically see your posts. On my website, you have to remember to visit.</p>
<p>To bridge this gap, I first added <a href="https://dri.es/photos.xml">an RSS feed for my photos</a>. But since not everyone uses RSS, I later started <a href="https://buttondown.com/dries-buytaert-photos">a monthly photo newsletter</a>. Once a month, I pick my favorite photos, format them for email, and send them out.</p>
<p>After sending five or six photo newsletters, I could already feel my motivation fading. Each one only took about twenty minutes to make, but it still felt like too much friction. So, I decided to fix that.</p>
<p>Under the hood, my photo stream runs on <a href="https://www.drupal.org">Drupal</a>, built as a custom module. I added two new routes to my custom Drupal module:</p>
<ul>
<li><code>/photos/$year/$month</code>: shows all photos for a given month, with the usual features: lazy loading, responsive images, <a href="https://schema.org/">Schema.org</a> markup, and so on.</li>
<li><code>/photos/$year/$month?email=true</code>: shows the same photos, but stripped down and formatted specifically for email clients.</li>
</ul>
<p>Now my monthly workflow looks like this: visit <code>/photos/2025/9?email=true</code>, copy the source HTML, paste it into <a href="https://buttondown.com/">Buttondown</a>, and hit 'Send'. That twenty-minute task became a one-minute task.</p>
<p>I spent two hours coding this to save nineteen minutes a month. In other words, it takes about six months before the time saved equals the time invested. The math checks out: 120 / 19 ≈ 6. My developer brain called it a win. My business brain called it a write-off.</p>
<p>But the real benefit isn't the time saved. The easier something is, the more likely I am to stick with it. Automation doesn't just save time. It also preserves good intentions.</p>
<p>Could I automate this completely? Sure. I'm a developer, remember. Maybe I will someday. But for now, that one-minute task each month feels right. It's just enough involvement to stay connected to what I'm sharing, without the friction that kills motivation.</p>
]]></description>
    </item>
    <item>
      <title>A photo stream for my site</title>
      <link>https://dri.es/a-photo-stream-for-my-site</link>
      <guid>https://dri.es/a-photo-stream-for-my-site</guid>
      <pubDate>Sun, 06 Nov 2022 11:35:54 -0500</pubDate>
      <description><![CDATA[<p>It's no secret that I don't like using Facebook, Instagram or Twitter as my primary platform for sharing photos and status updates. I don't trust these platforms with my data, and I don't like that they track my friends and family each time I post something.</p>
<p>For those reasons, I set a personal challenge in 2018 to <a href="https://dri.es/taking-control-of-my-data-and-social-media">take back control over my social media, photos and more</a>. As a result, <a href="https://dri.es/pulling-the-plug-on-facebook">I quit Facebook</a> and stopped using <a href="https://twitter.com/Dries">Twitter</a> for personal status updates. I still use Twitter for <a href="https://www.drupal.org/">Drupal</a>-related updates.</p>
<p>To this date, I still occasionally post on Instagram. The main reason I still post on Instagram is that it's simply the easiest way for friends and family to follow me. But every time I post a photo on Instagram, <a href="https://dri.es/state-of-drupal-presentation-september-2022">I cringe</a>. I don't like that I cause friends and family to be tracked.</p>
<p>My workflow is to upload photos to my website first. After uploading photos to my website, I occasionally <a href="https://indieweb.org/POSSE">POSSE</a> a photo to <a href="https://www.instagram.com/dries.buytaert/">Instagram</a>. I have used this workflow for many years. As a result, I have over 10,000 photos on my website, and only 300 photos on Instagram. By all means, my website is my primary platform for sharing photos.</p>
<p>I decided it was time to make it a bit easier for people to follow my photography and adventures from my website. Last month I silently added <a href="https://dri.es/photos">a photo stream</a>, complete with <a href="https://dri.es/photos.xml">an RSS feed</a> and <a href="https://buttondown.com/dries-buytaert-photos">mailing list</a>. Now there is a page that shows my newest photos and an easy way to subscribe to them.</p>
<p>While an RSS feed doesn't have the same convenience factor as Instagram, it <em>is</em> better than Instagram in the following ways: more photos, no tracking, no algorithmic filtering, no account required, and no advertising.</p>
<p class="pullquote">My photo stream and photo galleries aspire to the privacy of a printed photo album.</p>
<p>I encourage you to subscribe to my photo stream and to unfollow me on Instagram.</p>
<p>Step by step, I will continue to build my audience here, on my blog, on the edge of the <a href="https://dri.es/tag/open-web">Open Web</a>, on my own terms.</p>
<p>From a technical point of view, my photo stream uses <a href="https://dri.es/responsive-images-for-dri-es">responsive images</a> that are <a href="https://dri.es/optimizing-site-performance-by-lazy-loading-images">lazy loaded</a> – it should be pretty fast. And it uses the <a href="https://ogp.me/">Open Graph Protocol</a> for improved link sharing.</p>
<p>PS: The photo at the top is the subway in Boston. Taken on my way home from work.</p>
<p>
  <a href="https://news.indieweb.org/en" class="u-syndication">Also posted on IndieNews</a>.</p>
]]></description>
    </item>
  </channel>
</rss>
