<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home - tcole.net</title>
    <meta name="description" content="Travis Cole&#39;s personal site and blog">
    <link rel="stylesheet" href="/assets/css/style.css">
    
  </head>

  <body>
    <header>
  <div class="site-name">
    <a href="/">tcole.net</a>
  </div>
  <nav>
    <ul class="menu">
      <li><a href="/" aria-current="page">Home</a></li>
      <li><a href="/about/" >About</a></li>
      <li><a href="/blog/" >Blog</a></li>
      <li><a href="/projects/" >Projects</a></li>
    </ul>
  </nav>
</header>
    
    <main>
      <div class="home">
  
    
    <article class="full-post">
      <header>
        <h1>Vibe Coding: Auto-Crossposting from Jekyll to Bluesky</h1>
        <p>
          <time datetime="2025-03-29T00:00:00-07:00">Mar 29, 2025</time>
          
            <span class="categories">
              in 
              
                <a href="/categories/ai/">ai</a>
              
                <a href="/categories/projects/">projects</a>
              
                <a href="/categories/tools/">tools</a>
              
            </span>
          
        </p>
      </header>
      <p>Last night I automated cross-posting from my Jekyll blog to my Bluesky account
using GitHub Actions and a Python script. This was a straightforward project
done entirely with
<a href="https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview">Claude Code</a>,
which is Anthropic’s coding agent, released on February 24th, 2025.</p>

<h2 id="the-requirements">The Requirements</h2>

<p>I wanted a system that would:</p>

<ol>
  <li>Detect new posts in my Jekyll blog, hosted on GitHub Pages</li>
  <li>Create a Bluesky post with a clickable link to my blog</li>
  <li>Include a brief summary of the post</li>
  <li>Skip any draft posts</li>
  <li>Only post each article once</li>
</ol>

<h2 id="the-solution-github-actions--atproto">The Solution: GitHub Actions + atproto</h2>

<p>I asked Claude Code to design a solution using
<a href="https://atproto.blue/">ATProto</a> and discuss it with me before we started
writing code. We went back and forth about how to track what had already been
posted, but settled on a simple solution: have GitHub Actions commit a JSON
file that tracks posts.</p>

<p>For this project, we built:</p>

<ol>
  <li>A GitHub Actions workflow (<code class="language-plaintext highlighter-rouge">.github/workflows/bluesky-crosspost.yml</code>)</li>
  <li>A Python script for the cross-posting logic
(<code class="language-plaintext highlighter-rouge">.github/scripts/crosspost_to_bluesky.py</code>)</li>
  <li>A JSON tracking file (<code class="language-plaintext highlighter-rouge">.github/bluesky-published.json</code>) committed to git by
the GitHub Action</li>
</ol>

<p>I didn’t write a line of code myself - Claude handled everything from the
workflow configuration to the Python implementation.</p>

<h3 id="the-github-action-workflow">The GitHub Action Workflow</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Cross-post to Bluesky</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">main</span><span class="pi">]</span>
    <span class="na">paths</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">_posts/**"</span>

  <span class="c1"># Allow manual triggering</span>
  <span class="na">workflow_dispatch</span><span class="pi">:</span>

<span class="c1"># Set permissions for the GITHUB_TOKEN</span>
<span class="na">permissions</span><span class="pi">:</span>
  <span class="na">contents</span><span class="pi">:</span> <span class="s">write</span> <span class="c1"># Needed to push the updated JSON file</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">crosspost</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="c1"># Run for push events or manual triggers</span>
    <span class="na">if</span><span class="pi">:</span>
      <span class="s">github.event_name == 'push' || github.event_name == 'workflow_dispatch'</span>

    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout repository</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">fetch-depth</span><span class="pi">:</span> <span class="m">0</span> <span class="c1"># Fetch all history</span>
          <span class="na">token</span><span class="pi">:</span> <span class="s">$</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up Python</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-python@v5</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">python-version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.11"</span>
          <span class="na">cache</span><span class="pi">:</span> <span class="s2">"</span><span class="s">pip"</span>
          <span class="na">cache-dependency-path</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">.github/requirements.txt</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install dependencies</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">python -m pip install --upgrade pip</span>
          <span class="s">pip install -r .github/requirements.txt</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Find changed posts</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">changed-posts</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">if [[ "$" == "workflow_dispatch" ]]; then</span>
            <span class="s"># When manually triggered, use the latest post</span>
            <span class="s">LATEST_POST=$(find _posts -type f -name "*.markdown" -o -name "*.md" | sort -r | head -n1)</span>
            <span class="s">echo "Using latest post: $LATEST_POST"</span>
            <span class="s">echo "changed_files=$LATEST_POST" &gt;&gt; $GITHUB_OUTPUT</span>
          <span class="s">else</span>
            <span class="s"># Get list of changed files from the push event</span>
            <span class="s">CHANGED_FILES=$(git diff --name-only $ $ | grep "_posts/" | tr '\n' ' ')</span>
            <span class="s">echo "Changed files: $CHANGED_FILES"</span>
            <span class="s">echo "changed_files=$CHANGED_FILES" &gt;&gt; $GITHUB_OUTPUT</span>
          <span class="s">fi</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Crosspost to Bluesky</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">steps.changed-posts.outputs.changed_files != ''</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">BLUESKY_IDENTIFIER</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">BLUESKY_PASSWORD</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">ANTHROPIC_API_KEY</span><span class="pi">:</span> <span class="s">$</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">python .github/scripts/crosspost_to_bluesky.py $</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Commit updated published list</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">steps.changed-posts.outputs.changed_files != ''</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">git config --local user.email "github-actions[bot]@users.noreply.github.com"</span>
          <span class="s">git config --local user.name "github-actions[bot]"</span>
          <span class="s">git add .github/bluesky-published.json</span>
          <span class="s">git commit -m "Update Bluesky published posts list [skip ci]" || echo "No changes to commit"</span>
          <span class="s">git push</span>
</code></pre></div></div>

<p>The workflow is triggered either when changes are pushed to the <code class="language-plaintext highlighter-rouge">_posts/</code>
directory or when manually triggered. It identifies which post files were
changed, runs the Python script to cross-post them, and then commits the
updated tracking file.</p>

<h3 id="python-dependencies">Python Dependencies</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>atproto&gt;=0.0.31
PyYAML&gt;=6.0
python-frontmatter&gt;=1.0.0
markdown&gt;=3.4.0
anthropic&gt;=0.5.0
</code></pre></div></div>

<h3 id="the-cross-posting-script">The Cross-posting Script</h3>

<p>The most interesting part of the implementation is the creation of a rich text
link in Bluesky, which uses a feature called “facets”:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">post_to_bluesky</span><span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="n">summary</span><span class="p">,</span> <span class="n">post_url</span><span class="p">,</span> <span class="n">categories</span><span class="p">):</span>
    <span class="s">"""Post to Bluesky with the blog post summary and link."""</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="c1"># Format post with title as clickable link
</span>        <span class="n">intro</span> <span class="o">=</span> <span class="s">"A new post on my blog:</span><span class="se">\n\n</span><span class="s">"</span>
        <span class="n">post_text</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">intro</span><span class="si">}{</span><span class="n">title</span><span class="si">}</span><span class="se">\n\n</span><span class="si">{</span><span class="n">summary</span><span class="si">}</span><span class="s">"</span>

        <span class="c1"># Create rich text facets for the title to make it a clickable link
</span>        <span class="n">intro_bytes_length</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">intro</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="s">'utf-8'</span><span class="p">))</span>
        <span class="n">title_bytes_length</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">title</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="s">'utf-8'</span><span class="p">))</span>

        <span class="n">facets</span> <span class="o">=</span> <span class="p">[</span>
            <span class="p">{</span>
                <span class="s">"index"</span><span class="p">:</span> <span class="p">{</span>
                    <span class="s">"byteStart"</span><span class="p">:</span> <span class="n">intro_bytes_length</span><span class="p">,</span>
                    <span class="s">"byteEnd"</span><span class="p">:</span> <span class="n">intro_bytes_length</span> <span class="o">+</span> <span class="n">title_bytes_length</span>
                <span class="p">},</span>
                <span class="s">"features"</span><span class="p">:</span> <span class="p">[</span>
                    <span class="p">{</span>
                        <span class="s">"$type"</span><span class="p">:</span> <span class="s">"app.bsky.richtext.facet#link"</span><span class="p">,</span>
                        <span class="s">"uri"</span><span class="p">:</span> <span class="n">post_url</span>
                    <span class="p">}</span>
                <span class="p">]</span>
            <span class="p">}</span>
        <span class="p">]</span>

        <span class="c1"># Create the post with rich text
</span>        <span class="n">client</span><span class="p">.</span><span class="n">com</span><span class="p">.</span><span class="n">atproto</span><span class="p">.</span><span class="n">repo</span><span class="p">.</span><span class="n">create_record</span><span class="p">({</span>
            <span class="s">"repo"</span><span class="p">:</span> <span class="n">client</span><span class="p">.</span><span class="n">me</span><span class="p">.</span><span class="n">did</span><span class="p">,</span>
            <span class="s">"collection"</span><span class="p">:</span> <span class="s">"app.bsky.feed.post"</span><span class="p">,</span>
            <span class="s">"record"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"$type"</span><span class="p">:</span> <span class="s">"app.bsky.feed.post"</span><span class="p">,</span>
                <span class="s">"text"</span><span class="p">:</span> <span class="n">post_text</span><span class="p">,</span>
                <span class="s">"facets"</span><span class="p">:</span> <span class="n">facets</span><span class="p">,</span>
                <span class="s">"createdAt"</span><span class="p">:</span> <span class="n">datetime</span><span class="p">.</span><span class="n">now</span><span class="p">(</span><span class="n">timezone</span><span class="p">.</span><span class="n">utc</span><span class="p">).</span><span class="n">isoformat</span><span class="p">().</span><span class="n">replace</span><span class="p">(</span><span class="s">'+00:00'</span><span class="p">,</span> <span class="s">'Z'</span><span class="p">),</span>
            <span class="p">}</span>
        <span class="p">})</span>

        <span class="k">return</span> <span class="bp">True</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Error posting to Bluesky: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
        <span class="k">return</span> <span class="bp">False</span>
</code></pre></div></div>

<p>The script also detects whether I’m commenting on someone else’s blog post or
writing original content, and formats the summary accordingly.</p>

<h2 id="generating-summaries-with-claude">Generating Summaries with Claude</h2>

<p>At first we were just using the first few lines of the post for a summary, but
I didn’t like the result and realized we could use AI to make the summary.</p>

<p>So we added summarization using the Anthropic API. I asked Claude Chat to
recommend a model for this use case, and it suggested Claude Haiku 3.5 for its
low cost and speed. The first few tries sounded like pretty bad marketing
speak, so we had to iterate several times on the prompt. We settled on
something that I think works pretty well.</p>

<p>This creates more natural-sounding summaries that match my writing style:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">generate_summary_with_claude</span><span class="p">(</span><span class="n">title</span><span class="p">,</span> <span class="n">content</span><span class="p">,</span> <span class="n">max_length</span><span class="p">):</span>
    <span class="s">"""Generate a summary using Claude via the Anthropic API."""</span>
    <span class="n">client</span> <span class="o">=</span> <span class="n">anthropic</span><span class="p">.</span><span class="n">Anthropic</span><span class="p">(</span><span class="n">api_key</span><span class="o">=</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"ANTHROPIC_API_KEY"</span><span class="p">))</span>

    <span class="c1"># Detect if this is commenting on someone else's post
</span>    <span class="n">has_quotes</span> <span class="o">=</span> <span class="s">'&gt;'</span> <span class="ow">in</span> <span class="n">content</span>
    <span class="n">attribution_pattern</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="n">search</span><span class="p">(</span><span class="sa">r</span><span class="s">'([A-Z][a-z]+ [A-Z][a-z]+) on \[(.*?)\]'</span><span class="p">,</span> <span class="n">content</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">has_quotes</span> <span class="ow">or</span> <span class="n">attribution_pattern</span><span class="p">:</span>
        <span class="c1"># Format prompt for reference posts
</span>        <span class="c1"># ...
</span>    <span class="k">else</span><span class="p">:</span>
        <span class="c1"># Format prompt for original content
</span>        <span class="c1"># ...
</span>
    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">messages</span><span class="p">.</span><span class="n">create</span><span class="p">(</span>
        <span class="n">model</span><span class="o">=</span><span class="s">"claude-3-5-haiku-latest"</span><span class="p">,</span>
        <span class="n">max_tokens</span><span class="o">=</span><span class="mi">300</span><span class="p">,</span>
        <span class="n">temperature</span><span class="o">=</span><span class="mf">0.2</span><span class="p">,</span>
        <span class="n">system</span><span class="o">=</span><span class="s">"You extract only the core facts in plainest possible language"</span><span class="p">,</span>
        <span class="n">messages</span><span class="o">=</span><span class="p">[{</span><span class="s">"role"</span><span class="p">:</span> <span class="s">"user"</span><span class="p">,</span> <span class="s">"content"</span><span class="p">:</span> <span class="n">prompt</span><span class="p">}]</span>
    <span class="p">)</span>

    <span class="k">return</span> <span class="n">response</span><span class="p">.</span><span class="n">content</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">text</span><span class="p">.</span><span class="n">strip</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="results">Results</h2>

<p>Posts on Bluesky now look like:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>A new post on my blog:

Automated Cross-posting from Jekyll to Bluesky

A GitHub Action that detects new blog posts and cross-posts them to Bluesky
with clickable links and AI-generated summaries.
</code></pre></div></div>

<p>The title becomes a clickable link to the original blog post, and the summary
is generated automatically using Claude.</p>

<h2 id="challenges-and-learnings">Challenges and Learnings</h2>

<p>Claude thinks the most challenging part was:</p>

<blockquote>
  <p>…getting the Bluesky facets (rich text) working correctly. The byte
indexing for the clickable link was tricky, and we had to handle UTF-8
encoding properly.</p>
</blockquote>

<p>But I basically asked it to write the code, then asked it to check for bugs,
which prompted Claude to change a lot of things. Then we tested the workflow
live. It failed on the first try, but Claude Code used the GitHub <code class="language-plaintext highlighter-rouge">gh</code> CLI to
check the GitHub Action run for errors. It fixed the issues and then we had
things working.</p>

<p>The AI summary took about 15 minutes to refine. We settled on a system prompt
(“extract only the core facts in plainest possible language”) that generates
direct, factual summaries matching my writing style.</p>

<h2 id="trying-it-yourself">Trying It Yourself</h2>

<p>If you want to implement this for your own Jekyll blog, you’ll need:</p>

<ol>
  <li>A GitHub repository with Jekyll</li>
  <li>A Bluesky account</li>
  <li>GitHub repository secrets for BLUESKY_IDENTIFIER and BLUESKY_PASSWORD</li>
  <li>Optional: An Anthropic API key for better summaries</li>
</ol>

<p>The full code is available in
<a href="https://github.com/kelp/kelp.github.io">my blog’s repository</a>.</p>

      <p class="all-posts-link">
        <a href="/blog/">View all posts →</a>
      </p>
    </article>
  
</div>
    </main>
    
    <footer>
  <p>© tcole.net 2026</p>
  <div class="social">
    
    <a href="https://github.com/kelp" target="_blank" rel="me" aria-label="GitHub Profile">
      <svg class="icon" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
        <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" fill="currentColor"></path>
      </svg>
    </a>
    
    
    <a href="https://bsky.app/profile/tcole.net" target="_blank" rel="me" aria-label="Bluesky Profile">
      <svg class="icon" width="16" height="14" viewBox="0 0 600 530" aria-hidden="true">
        <path d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z" fill="currentColor"/>
      </svg>
    </a>
    
    
    <a href="https://hachyderm.io/@kelp" target="_blank" rel="me" aria-label="Mastodon Profile">
      <svg class="icon" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
        <path d="M21.327 8.566c0-4.339-2.843-5.61-2.843-5.61-1.433-.658-3.894-.935-6.451-.956h-.063c-2.557.021-5.016.298-6.45.956 0 0-2.843 1.272-2.843 5.61 0 .993-.019 2.181.012 3.441.103 4.243.778 8.425 4.701 9.463 1.809.479 3.362.579 4.612.51 2.268-.126 3.538-.809 3.538-.809l-.075-1.646s-1.621.511-3.441.449c-1.804-.062-3.707-.194-3.999-2.409a4.52 4.52 0 0 1-.04-.621s1.77.433 4.014.536c1.372.063 2.658-.08 3.965-.236 2.506-.299 4.688-1.843 4.962-3.254.434-2.223.398-5.424.398-5.424zm-3.353 5.59h-2.081V9.057c0-1.075-.452-1.62-1.357-1.62-1 0-1.501.647-1.501 1.927v2.791h-2.069V9.364c0-1.28-.501-1.927-1.502-1.927-.905 0-1.357.546-1.357 1.62v5.099H6.026V8.903c0-1.074.273-1.927.823-2.558.566-.631 1.307-.955 2.228-.955 1.065 0 1.872.409 2.405 1.228l.518.869.519-.869c.533-.819 1.34-1.228 2.405-1.228.92 0 1.662.324 2.228.955.549.631.822 1.484.822 2.558v5.253z" fill="currentColor"/>
      </svg>
    </a>
    
    
    <a href="https://www.linkedin.com/in/traviscole/" target="_blank" rel="me" aria-label="LinkedIn Profile">
      <svg class="icon" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
        <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" fill="currentColor"/>
      </svg>
    </a>
    
    
    <a href="mailto:kelp@plek.org" target="_blank" aria-label="Email">
      <svg class="icon" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
        <path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z" fill="currentColor"/>
      </svg>
    </a>
    
  </div>
</footer>
  </body>
</html>