<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: David Loor</title>
    <description>The latest articles on DEV Community by David Loor (@davo20019).</description>
    <link>https://dev.to/davo20019</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F137330%2F76388a37-9c3a-4f63-9ebd-ef654392a0e1.jpg</url>
      <title>DEV Community: David Loor</title>
      <link>https://dev.to/davo20019</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/davo20019"/>
    <language>en</language>
    <item>
      <title>Putting my step count on my website</title>
      <dc:creator>David Loor</dc:creator>
      <pubDate>Mon, 03 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/davo20019/putting-my-step-count-on-my-website-30li</link>
      <guid>https://dev.to/davo20019/putting-my-step-count-on-my-website-30li</guid>
      <description>&lt;p&gt;I spent the weekend adding my step count to my website. Now everyone can see how much I walk.&lt;/p&gt;

&lt;p&gt;My smartwatch and Android phone track my steps throughout the day. Since they're Android devices, all that data gets stored in Google Fit. I use Google Cloud's API to pull that data and display it on my site.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it shows
&lt;/h2&gt;

&lt;p&gt;Just the basics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Today's steps (shown in the footer of every page)&lt;/li&gt;
&lt;li&gt;Weekly total steps&lt;/li&gt;
&lt;li&gt;30-day total&lt;/li&gt;
&lt;li&gt;My streak&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. Just steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I'm not a fitness person. I don't have fitness routines or step goals. But I think public accountability might work to make me walk or exercise more.&lt;/p&gt;

&lt;p&gt;Also, I wanted to implement an API integration with Google Fit and this seemed like a quick pet project to try it out.&lt;/p&gt;

&lt;p&gt;I got inspired by &lt;a href="https://dri.es/posting-my-phone-battery-status-to-my-site" rel="noopener noreferrer"&gt;Dries Buytaert&lt;/a&gt; who posts his phone battery status, and &lt;a href="https://aaronparecki.com/sleep" rel="noopener noreferrer"&gt;Aaron Parecki&lt;/a&gt; who tracks his &lt;a href="https://aaronparecki.com/sleep" rel="noopener noreferrer"&gt;sleep&lt;/a&gt; and &lt;a href="https://aaronparecki.com/health" rel="noopener noreferrer"&gt;health&lt;/a&gt; publicly. I'm doing the same thing with my steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;I connected my Google account to pull data from Google Fit, made a simple dashboard to see my stats, and added a page to display them. I can turn it on or off whenever.&lt;/p&gt;

&lt;p&gt;The data refreshes automatically every hour using a Cloudflare Worker cron job, so the numbers stay current throughout the day without me having to do anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tech side
&lt;/h2&gt;

&lt;p&gt;If you want to build something like this, here's the basic flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set up a Google Cloud project and enable the Fitness API&lt;/li&gt;
&lt;li&gt;Configure OAuth 2.0 credentials to authenticate users&lt;/li&gt;
&lt;li&gt;Use the Google Fit API to fetch step data from the dataset.aggregate endpoint&lt;/li&gt;
&lt;li&gt;Store access and refresh tokens securely in Cloudflare KV&lt;/li&gt;
&lt;li&gt;Calculate stats like weekly totals, monthly totals, and streaks from the raw data&lt;/li&gt;
&lt;li&gt;Build a simple UI to display the data&lt;/li&gt;
&lt;li&gt;Set up a Cloudflare Worker with a cron trigger to refresh data automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I built this with Next.js and TypeScript, hosted on Cloudflare Pages. The authentication and API endpoints run as a separate Cloudflare Worker, while the frontend shows the stats and charts.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it live
&lt;/h2&gt;

&lt;p&gt;Check out &lt;a href="https://dev.to/steps"&gt;my steps page&lt;/a&gt; to see the tracker in action.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>apiintegration</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>How to run open source LLMs (AI) on your computer?</title>
      <dc:creator>David Loor</dc:creator>
      <pubDate>Sat, 01 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/davo20019/how-to-run-open-source-llms-ai-on-your-computer-43n</link>
      <guid>https://dev.to/davo20019/how-to-run-open-source-llms-ai-on-your-computer-43n</guid>
      <description>&lt;p&gt;I set up my own AI assistant that runs completely on my computer. No internet connection needed. No data sent to the cloud.&lt;/p&gt;

&lt;p&gt;If you've been curious about running your own AI but thought it required a degree in computer science, I have good news: it's easier than you think.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why would you want a local LLM (AI)?
&lt;/h2&gt;

&lt;p&gt;Before we dive into the how, let's talk about the why. Running LLMs (AI) locally has some real advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy&lt;/strong&gt; : Your conversations never leave your computer. No company is storing, analyzing, or training on your data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No internet required&lt;/strong&gt; : Once set up, you can use your AI anywhere: on a plane, in a coffee shop with spotty WiFi, or in the middle of nowhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's free&lt;/strong&gt; : After the initial setup, there are no subscription fees or API costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's yours&lt;/strong&gt; : You control everything. No rate limits, no content filters, no terms of service changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you'll need
&lt;/h2&gt;

&lt;p&gt;Here's what I needed to get started (and you'll need the same):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A reasonably modern computer (Windows, Mac, or Linux)&lt;/li&gt;
&lt;li&gt;About 30 minutes of your time&lt;/li&gt;
&lt;li&gt;Around 4-8 GB of free disk space (depending on which model you choose)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. No expensive GPU required, though if you have one, things will run faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is llama.cpp?
&lt;/h2&gt;

&lt;p&gt;Before we dive into installation, let me explain what we're actually installing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ggerganov/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt; is software that lets you run Large Language Models (AI) on your computer. Think of it as the engine that makes everything work. These models are just data files, like videos or music files. llama.cpp is the player that knows how to use them.&lt;/p&gt;

&lt;p&gt;It's open source, free, and actively maintained. It works on Mac, Windows, and Linux, and it's designed to be fast even on regular computers without fancy graphics cards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;The easiest way to install llama.cpp is through a package manager.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you're on a Mac or Linux
&lt;/h3&gt;

&lt;p&gt;Open your terminal and type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew install llama.cpp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the installation. Seriously.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you're on Windows
&lt;/h3&gt;

&lt;p&gt;Open PowerShell and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;winget install llama.cpp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running your first AI model
&lt;/h2&gt;

&lt;p&gt;This is where it gets exciting. You don't need to download model files manually or figure out where to put them. The tool does it all for you.&lt;/p&gt;

&lt;p&gt;Here's what you type to start chatting with an AI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;llama-cli -hf bartowski/Llama-3.2-3B-Instruct-GGUF:Q8_0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This downloads and runs the &lt;a href="https://huggingface.co/bartowski/Llama-3.2-3B-Instruct-GGUF" rel="noopener noreferrer"&gt;Llama 3.2 3B model&lt;/a&gt; from Hugging Face.&lt;/p&gt;

&lt;p&gt;Let me break down what this does in plain English:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;llama-cli&lt;/code&gt; starts the chat interface&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-hf&lt;/code&gt; tells it to download from &lt;a href="https://huggingface.co/" rel="noopener noreferrer"&gt;Hugging Face&lt;/a&gt; (a repository of LLMs)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bartowski/Llama-3.2-3B-Instruct-GGUF:Q8_0&lt;/code&gt; is the specific model to use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model identifier has three parts separated by slashes and a colon:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bartowski&lt;/code&gt; is the username of who uploaded the model on Hugging Face&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Llama-3.2-3B-Instruct-GGUF&lt;/code&gt; is the repository name containing the model&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Q8_0&lt;/code&gt; is the specific quantization version (quality vs. size)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you run this command, the model downloads automatically (this might take a few minutes the first time), and then you can start chatting. Type your questions, and the AI responds right there in your terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to find and use different models
&lt;/h2&gt;

&lt;p&gt;The model I mentioned above (Llama 3.2) is a good starting point. It's relatively small and fast, even on modest hardware. But there are hundreds of other models you can try.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding models on Hugging Face
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/docs/hub/en/gguf" rel="noopener noreferrer"&gt;GGUF&lt;/a&gt; is the file format that llama.cpp uses. Models on Hugging Face come in different formats, but llama.cpp specifically needs GGUF files.&lt;/p&gt;

&lt;p&gt;Go to &lt;a href="https://huggingface.co/models" rel="noopener noreferrer"&gt;huggingface.co/models&lt;/a&gt; and search for GGUF. Look for repositories that end with -GGUF in the name.&lt;/p&gt;

&lt;p&gt;When you find a model you want to try, click on it and look for the repository name. For example, if you see bartowski/Qwen2.5-7B-Instruct-GGUF, you can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;llama-cli -hf bartowski/Qwen2.5-7B-Instruct-GGUF:Q8_0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tips for choosing models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smaller numbers = faster, but less capable&lt;/strong&gt; : A 3B model is faster than a 7B model, which is faster than a 13B model. The number refers to how many billions of parameters the model has.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look for "Instruct" or "Chat" in the name&lt;/strong&gt; : These models are specifically trained for conversations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The :Q8_0 part&lt;/strong&gt; : This is the quantization level. Q8_0 is a good balance of quality and size. Q4_0 is smaller/faster but slightly lower quality.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Taking it a step further: Running an AI server
&lt;/h2&gt;

&lt;p&gt;Once you're comfortable with the basic chat, you can level up by running your AI as a server. This lets you chat with it from your web browser and use it with other apps.&lt;/p&gt;

&lt;p&gt;Run this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;llama-server -hf bartowski/Llama-3.2-3B-Instruct-GGUF:Q8_0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see output like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;main: server is listening on http://127.0.0.1:8080 - starting the main loop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now open &lt;code&gt;http://127.0.0.1:8080&lt;/code&gt; (or &lt;code&gt;http://localhost:8080&lt;/code&gt;) in your web browser. You'll see a chat interface where you can talk to your AI directly from the browser. No additional software needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5vy2kxiligvcgokttqr6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5vy2kxiligvcgokttqr6.png" alt="llama.cpp web interface showing a clean chat interface with dark theme" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The server also works with any app that supports &lt;a href="https://platform.openai.com/docs/api-reference/introduction" rel="noopener noreferrer"&gt;OpenAI's API format&lt;/a&gt;. This means you can use tools like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Continue.dev (AI coding assistant in VS Code)&lt;/li&gt;
&lt;li&gt;Open WebUI (a ChatGPT-like interface)&lt;/li&gt;
&lt;li&gt;Any other app that supports custom API endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where the models are stored
&lt;/h2&gt;

&lt;p&gt;When you run these commands, models download automatically to a cache folder. By default, it's in your home directory under &lt;code&gt;.cache/llama.cpp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want to change where models are stored, you can set an environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# On Mac/Linuxexport LLAMA_CACHE=/path/to/your/cache# On Windows (PowerShell)$env:LLAMA_CACHE="C:\path\to\your\cache"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What about performance?
&lt;/h2&gt;

&lt;p&gt;Here's what I learned about performance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On newer Macs (M1/M2/M3/M4)&lt;/strong&gt;: Models run surprisingly fast. The M-series chips have built-in AI acceleration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On Windows/Linux with a decent GPU&lt;/strong&gt; : Also fast. If you have an NVIDIA GPU, llama.cpp will use it automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On older computers or laptops without GPUs&lt;/strong&gt; : Still works! Just stick with smaller models (3B or 7B) and expect slower responses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For reference, on my M4 Pro with 48GB RAM, a 3B model generates text at about 60-65 tokens per second. That's fast enough to feel like a real conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  llama.cpp vs Ollama vs vLLM
&lt;/h2&gt;

&lt;p&gt;You might have heard of other tools like Ollama or vLLM. Here's how they compare:&lt;/p&gt;

&lt;h3&gt;
  
  
  Ollama
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; is built on top of llama.cpp. It adds a simpler interface and automatic model management, but that convenience comes with performance overhead. In my tests on an M4 Pro Mac with 48GB RAM, llama.cpp was an order of magnitude faster than Ollama running the same models.&lt;/p&gt;

&lt;p&gt;If you want maximum performance and don't mind using the command line, stick with llama.cpp. If you prefer easier model management and can accept slightly slower speeds, Ollama works well.&lt;/p&gt;

&lt;h3&gt;
  
  
  vLLM
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/vllm-project/vllm" rel="noopener noreferrer"&gt;vLLM&lt;/a&gt; doesn't support macOS. It's Linux-only and designed for high-throughput server deployments with multiple GPUs. If you're on a Mac, it's not an option.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bottom line
&lt;/h3&gt;

&lt;p&gt;For Mac users, especially on Apple Silicon (M1/M2/M3/M4), llama.cpp is the best choice. It's the fastest option and has native Metal acceleration. Ollama is fine if you want simpler management. vLLM isn't available on Mac.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions you might have
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is this legal?
&lt;/h3&gt;

&lt;p&gt;Yes. The models on Hugging Face are open source and free to use. Many are published by Meta (Facebook), Mistral AI, and other organizations that explicitly allow personal and commercial use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will this slow down my computer?
&lt;/h3&gt;

&lt;p&gt;While the AI is running, it uses CPU/GPU resources. But when you close it, everything returns to normal. It's like running any other application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use this for work?
&lt;/h3&gt;

&lt;p&gt;That depends on your work's policies. Since everything runs locally and your data doesn't leave your machine, it's generally safer than cloud AI. But check with your IT department first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to be online?
&lt;/h3&gt;

&lt;p&gt;Only for the initial download of models. After that, everything works completely offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  My experience
&lt;/h2&gt;

&lt;p&gt;I've been using my local AI, and I'm impressed. It's not quite as capable as GPT-5, Claude Sonnet 4.5, or Gemini 2.5, but for many tasks (writing emails, brainstorming ideas, answering questions) it's more than good enough.&lt;/p&gt;

&lt;p&gt;What I appreciate most is the privacy aspect. My conversations stay on my machine. No data sent to external servers. No training datasets being fed somewhere. This makes it better suited for working with confidential information compared to cloud AI, though you should still follow your organization's security policies.&lt;/p&gt;

&lt;p&gt;Is it perfect? No. Responses can sometimes be slow if I'm running a larger model. And the quality isn't quite at the level of the best cloud AIs. But for a free, private, always-available assistant? I'll take that trade-off.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>privacy</category>
    </item>
    <item>
      <title>How to manage multiple Drupal sites with one MCP server</title>
      <dc:creator>David Loor</dc:creator>
      <pubDate>Sat, 25 Oct 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/davo20019/how-to-manage-multiple-drupal-sites-with-one-mcp-server-c6g</link>
      <guid>https://dev.to/davo20019/how-to-manage-multiple-drupal-sites-with-one-mcp-server-c6g</guid>
      <description>&lt;h2&gt;
  
  
  The problem with context switching
&lt;/h2&gt;

&lt;p&gt;I work on a few Drupal sites. Every time I switch between them, there's this mental overhead: Which modules does this one have? What fields are on the article type? I need to pull some data from the database to display it. Should I create a new view, or does one already exist?&lt;/p&gt;

&lt;p&gt;Even with AI assistants like Claude Code, Cursor, or Windsurf, getting answers meant waiting while they ran multiple drush commands sequentially, piecing together fragments of information. So I built &lt;a href="https://github.com/davo20019/drupal-scout-mcp" rel="noopener noreferrer"&gt;Drupal Scout&lt;/a&gt;, an MCP server that gives AI assistants instant, comprehensive knowledge about your Drupal site. It's &lt;a href="https://github.com/davo20019/drupal-scout-mcp" rel="noopener noreferrer"&gt;open source on GitHub&lt;/a&gt;. Scout is read-only. It analyzes your site but never modifies your database or files.&lt;/p&gt;

&lt;p&gt;Without Scout, your AI runs commands like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI: Running drush field:list node article...AI: Running drush config:get field.field.node.article.field_image...AI: Running drush config:get field.field.node.article.field_category...# ...and so on, waiting between each command
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Scout, you just ask: "What fields are on the article type?"&lt;/p&gt;

&lt;p&gt;One call. Instant answer. Everything cross-referenced. &lt;strong&gt;This saves 70-90% of tokens&lt;/strong&gt; compared to running commands sequentially, and your AI responds instantly instead of waiting for multiple drush operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The multi-project trick
&lt;/h2&gt;

&lt;p&gt;Here's what I learned: You can use the same MCP server for all your projects. Each one gets its own context automatically.&lt;/p&gt;

&lt;p&gt;When I figured this out, it changed everything. No more switching configs. No more wondering which site I'm looking at. The AI just knows.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;Drupal Scout looks for a config.json file in your project directory. When you're in project A, it reads A's config. Switch to project B? It reads B's config. Simple.&lt;/p&gt;

&lt;p&gt;The MCP server itself lives in one place (I keep mine in ~/.local/drupal-scout-mcp), but each project tells it which Drupal installation to index.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting it up
&lt;/h3&gt;

&lt;p&gt;Install Drupal Scout once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/davo20019/drupal-scout-mcp.git ~/.local/drupal-scout-mcpcd ~/.local/drupal-scout-mcppython3 -m venv venvsource venv/bin/activatepip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in each Drupal project, drop two files in the root:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;config.json&lt;/strong&gt; (tells Scout where your Drupal lives):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ "drupal_root": "/path/to/your/drupal", "modules_path": "web/modules"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;.claude.json&lt;/strong&gt; (connects the MCP server):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claude mcp add --transport stdio drupal-scout -- \ ~/.local/drupal-scout-mcp/venv/bin/python \ ~/.local/drupal-scout-mcp/server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. When you cd into that project and start coding, Scout indexes those modules. Switch projects? It indexes the new ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can Drupal Scout do?
&lt;/h2&gt;

&lt;p&gt;Now that you have it set up, what can you actually do with it? Let me show you some real examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  Analyzing fields and content types
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You: "I need to add a phone number field to the staff content type"AI: Let me check if a phone field already exists...AI: Using drupal-scout get_field_info(field_name="phone")Result: Found 2 phone fields:• Phone Number (field_phone_number) Type: telephone, Used in: contact, vendor• Mobile Phone (field_mobile_phone) Type: telephone, Used in: employeeAI: "You can reuse field_phone_number on the staff content type!"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This saved you from creating a duplicate field with a different name. One query showed you all phone-related fields and where they're used.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding views and displays
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You: "Do we have any views that display user data?"AI: Using drupal-scout get_views_summary(entity_type="users")Result: Found 2 views:• User List (user_list) Displays: page, block• Staff Directory (staff_directory) Displays: page, blockAI: "Yes! The Staff Directory view already exists with filters for department and role."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of creating a new view, you discovered an existing one that does what you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleaning up taxonomy terms
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You: "Can I safely delete the 'Old Category' term?"AI: Using drupal-scout get_taxonomy_info(term_name="Old Category")Result: Term: Old Category (tid: 75)Usage: 0 content items, 0 viewsChildren: NoneSAFE TO DELETE - This term is not used anywhere.AI: "Yes, it's safe to delete. The term isn't used in any content or views."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now try asking about a term that &lt;em&gt;is&lt;/em&gt; used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You: "What about the 'Technology' term?"AI: Using drupal-scout get_taxonomy_info(term_name="Technology")Result: Term: Technology (tid: 42)Usage: 87 content itemsChildren: AI/ML, Web DevelopmentSample content:• How AI is Changing Development (nid: 123)• Tech Trends 2024 (nid: 156)... and 84 moreWARNING: Has child terms (2 children will become orphaned)CAUTION: Used in 87 pieces of contentAI: "This term is heavily used. Deleting it would affect 87 articles and orphan 2 child terms. Consider merging instead."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This analysis would have taken dozens of drush commands and database queries. Drupal Scout did it in one call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging with watchdog logs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You: "The site is showing errors, can you check what's wrong?"AI: Using drupal-scout get_watchdog_logs()Result: Found 12 PHP errors:[2024-01-15 14:32:21] Undefined array key 'field_category'Location: /modules/custom/custom_module/custom_module.module:45[2024-01-15 14:30:15] Call to undefined method getCategory()Location: /modules/custom/custom_module/src/Service/CategoryService.php:78AI: "I found two related errors in custom_module. Let me read those files and fix them..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI can immediately jump to the problem files and suggest fixes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting module recommendations
&lt;/h3&gt;

&lt;p&gt;Here's something I find really useful: Drupal Scout can search the Drupal.org API and recommend modules based on what you're trying to do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You: "I need a way to manage workflow and content moderation"AI: Using drupal-scout search_drupal_modules(keyword="workflow")Result: Found relevant modules:• Workbench Moderation - Provides editorial workflow for content• Content Moderation - Core workflow solution for Drupal 8+• Workflow - Flexible content workflow managementAI: "Based on your Drupal version and needs, I recommend Content Moderation since it's in core for Drupal 8+. Want me to check if it's already enabled?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of googling for modules or asking around, the AI can search Drupal.org, compare options, and make recommendations based on your specific setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips for getting the most out of Drupal Scout
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Ask broad questions first
&lt;/h3&gt;

&lt;p&gt;Instead of diving into implementation, ask Drupal Scout to check what exists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instead of: "Create a view showing recent articles"&lt;/li&gt;
&lt;li&gt;Try: "Do we have any views showing articles?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You might discover you already have what you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Let the AI use the tools
&lt;/h3&gt;

&lt;p&gt;You don't need to specify which MCP tool to use. Just ask natural questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What fields are on the article content type?"&lt;/li&gt;
&lt;li&gt;"Can I safely delete this module?"&lt;/li&gt;
&lt;li&gt;"Are there any recent errors in the logs?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI knows which tool to call based on your question.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Combine analysis with action
&lt;/h3&gt;

&lt;p&gt;Drupal Scout provides read-only analysis. When you need to make changes, the AI can use that information to execute commands safely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You: "Clean up unused modules"AI: Using drupal-scout find_unused_contrib()Result: Found 3 installed but unused modules: devel, kint, admin_toolbar_toolsAI: "I found 3 unused modules. Should I uninstall them?"You: "Yes"AI: Running: drush pmu devel kint admin_toolbar_toolsAI: Running: composer remove drupal/devel drupal/kint drupal/admin_toolbar_toolsAI: Using drupal-scout reindex_modules()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI uses Drupal Scout for analysis, then executes the necessary commands to make changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does MCP actually work?
&lt;/h2&gt;

&lt;p&gt;You might be wondering: "How does the AI know when to use Drupal Scout?" Great question! Understanding this helps you appreciate why MCP is so powerful.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tool description is the AI's instruction manual
&lt;/h3&gt;

&lt;p&gt;When Drupal Scout connects to your AI assistant, it doesn't just provide functions-it provides &lt;em&gt;documentation&lt;/em&gt; about those functions. Each tool has a description that tells the AI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the tool does&lt;/li&gt;
&lt;li&gt;When to use it&lt;/li&gt;
&lt;li&gt;What parameters it accepts&lt;/li&gt;
&lt;li&gt;What kind of results it returns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, the &lt;code&gt;get_field_info&lt;/code&gt; tool has a description like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Get comprehensive information about Drupal fields. **USE THIS TOOL** for questions about fields, where they're used,field types, and data structure.Examples:• "What fields does the article content type have?"• "Where is field_image used?"• "Do we have a phone number field?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This description becomes part of the AI's knowledge. When you ask "What fields are on my article content type?", the AI recognizes this matches the pattern and calls the appropriate tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  The conversation flow
&lt;/h3&gt;

&lt;p&gt;Here's what actually happens when you interact with Drupal Scout:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You ask&lt;/strong&gt; : "What fields does my article content type have?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI thinks&lt;/strong&gt; : "This is a question about fields. I have get_field_info for that."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI calls tool&lt;/strong&gt; : &lt;code&gt;drupal-scout.get_field_info(entity_type="node")&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP server executes&lt;/strong&gt; : Runs drush commands, parses configs, analyzes usage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP returns data&lt;/strong&gt; : "Fields Summary (15 fields found)..."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI formats response&lt;/strong&gt; : "I found 15 fields on your node entity types..."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The beauty is that you never see steps 2-5. You just get the answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this is better than traditional prompting
&lt;/h3&gt;

&lt;p&gt;Without MCP, you'd have to tell the AI exactly what to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You: "Run drush field-list, then for each field run drush config:get,then parse the YAML and tell me which are phone fields"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With MCP, you just ask naturally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You: "Do we have a phone field?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool descriptions taught the AI how to handle Drupal-specific questions. You don't need to be a drush expert-the MCP server already is.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's declarative, not imperative
&lt;/h3&gt;

&lt;p&gt;Here's a key insight: MCP tool descriptions are &lt;strong&gt;declarative&lt;/strong&gt; (describing what exists) rather than &lt;strong&gt;imperative&lt;/strong&gt; (forcing specific actions).&lt;/p&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The MCP doesn't override your prompts&lt;/li&gt;
&lt;li&gt;It tells the AI what capabilities are available&lt;/li&gt;
&lt;li&gt;The AI still decides when and how to use them&lt;/li&gt;
&lt;li&gt;You maintain full control through natural conversation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it like giving the AI a specialized toolkit. You still tell it what to build, but now it has better tools to build with.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Scout can analyze
&lt;/h2&gt;

&lt;p&gt;Drupal Scout provides deep analysis across your entire Drupal installation with 23+ specialized tools:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modules &amp;amp; Dependencies (8 tools):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search for functionality across custom and contrib modules&lt;/li&gt;
&lt;li&gt;List all modules with capabilities and hook implementations&lt;/li&gt;
&lt;li&gt;Get detailed module information (services, routes, classes, hooks)&lt;/li&gt;
&lt;li&gt;Analyze dependencies (forward, reverse, circular)&lt;/li&gt;
&lt;li&gt;Find unused contrib modules (with installation status)&lt;/li&gt;
&lt;li&gt;Check redundancy before building new features&lt;/li&gt;
&lt;li&gt;Find all implementations of a specific Drupal hook&lt;/li&gt;
&lt;li&gt;Force reindex when modules change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Entities, Fields &amp;amp; Content (multiple tools):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete entity structure with bundle information&lt;/li&gt;
&lt;li&gt;Comprehensive field analysis with usage tracking&lt;/li&gt;
&lt;li&gt;Entity references showing where content is used&lt;/li&gt;
&lt;li&gt;Display configurations for view modes&lt;/li&gt;
&lt;li&gt;Search entities by any field value&lt;/li&gt;
&lt;li&gt;Get entity info by ID or path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Views Analysis (1 tool):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;List all views with displays (page, block, feed, etc.)&lt;/li&gt;
&lt;li&gt;Display paths and settings&lt;/li&gt;
&lt;li&gt;Filters, sorts, and relationships&lt;/li&gt;
&lt;li&gt;Fields being displayed&lt;/li&gt;
&lt;li&gt;Filter by entity type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Taxonomy (2 tools):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All vocabularies with term counts and referencing fields&lt;/li&gt;
&lt;li&gt;Complete term hierarchies (parent/child relationships)&lt;/li&gt;
&lt;li&gt;Detailed usage analysis (content, views, fields)&lt;/li&gt;
&lt;li&gt;Safe-to-delete assessment with warnings&lt;/li&gt;
&lt;li&gt;Bulk analysis of entire vocabularies (optimized for large datasets)&lt;/li&gt;
&lt;li&gt;Export taxonomy data to CSV/Excel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security Scanning (11 tools):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL injection vulnerability detection&lt;/li&gt;
&lt;li&gt;Cross-Site Scripting (XSS) checks&lt;/li&gt;
&lt;li&gt;CSRF protection analysis&lt;/li&gt;
&lt;li&gt;Command injection detection&lt;/li&gt;
&lt;li&gt;Path traversal vulnerabilities&lt;/li&gt;
&lt;li&gt;Hardcoded secrets and API keys&lt;/li&gt;
&lt;li&gt;Access control issues&lt;/li&gt;
&lt;li&gt;Deprecated API usage&lt;/li&gt;
&lt;li&gt;Anonymous exploits (remotely exploitable)&lt;/li&gt;
&lt;li&gt;Security audit (comprehensive report)&lt;/li&gt;
&lt;li&gt;Verification guides (how to manually verify findings)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Code Analysis (4 tools):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read module files with smart chunking for large files&lt;/li&gt;
&lt;li&gt;List files in modules with size information&lt;/li&gt;
&lt;li&gt;Get module directory tree visualization&lt;/li&gt;
&lt;li&gt;Extract specific PHP functions from files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Drupal.org Integration (5 tools):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search for modules on Drupal.org&lt;/li&gt;
&lt;li&gt;Get popular modules by category&lt;/li&gt;
&lt;li&gt;Module recommendations based on needs&lt;/li&gt;
&lt;li&gt;Detailed module information with issue queue&lt;/li&gt;
&lt;li&gt;Search module issue queues for problems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;System Health &amp;amp; Monitoring (5 tools):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Watchdog logs with error patterns and diagnostics&lt;/li&gt;
&lt;li&gt;Database connectivity verification&lt;/li&gt;
&lt;li&gt;Available updates for core and contrib&lt;/li&gt;
&lt;li&gt;Full status report (/admin/reports/status equivalent)&lt;/li&gt;
&lt;li&gt;Configuration sync status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All 23+ tools work together seamlessly. The AI assistant automatically knows which tool to use based on your question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/davo20019/drupal-scout-mcp" rel="noopener noreferrer"&gt;Drupal Scout MCP on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/drupal-scout-mcp/" rel="noopener noreferrer"&gt;Drupal Scout on PyPI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.claude.com/en/docs/claude-code" rel="noopener noreferrer"&gt;Claude Code Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>drupal</category>
      <category>developertools</category>
    </item>
    <item>
      <title>How to Share Your Local WordPress or Drupal Site with Cloudflare Tunnel (Free)</title>
      <dc:creator>David Loor</dc:creator>
      <pubDate>Tue, 14 Oct 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/davo20019/how-to-share-your-local-wordpress-or-drupal-site-with-cloudflare-tunnel-free-2fd0</link>
      <guid>https://dev.to/davo20019/how-to-share-your-local-wordpress-or-drupal-site-with-cloudflare-tunnel-free-2fd0</guid>
      <description>&lt;p&gt;When developing locally with DDEV, you might need to share your work with clients, test webhooks from external services, or collaborate with remote team members. Cloudflare Tunnel provides a secure, simple way to expose your local development sites to the internet without opening firewall ports or dealing with complex networking configurations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Cloudflare Tunnel?
&lt;/h2&gt;

&lt;p&gt;Cloudflare Tunnel (formerly Argo Tunnel) is part of Cloudflare's Zero Trust networking solution. It creates a secure, outbound-only connection from your local machine to Cloudflare's edge network, which then routes public traffic to your local services. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No open inbound ports&lt;/strong&gt; - Your firewall stays secure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No public IP required&lt;/strong&gt; - Works behind NAT and corporate firewalls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free for development use&lt;/strong&gt; - Cloudflare's quick tunnels are free&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS by default&lt;/strong&gt; - Automatic SSL/TLS encryption&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple setup&lt;/strong&gt; - Just one command to get started&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is DDEV?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ddev.com/" rel="noopener noreferrer"&gt;DDEV&lt;/a&gt; is a Docker-based local development environment that makes it easy to set up PHP projects (Drupal, WordPress, Laravel, etc.) with minimal configuration. It provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pre-configured containers for web, database, and other services&lt;/li&gt;
&lt;li&gt;Support for multiple PHP versions&lt;/li&gt;
&lt;li&gt;Built-in SSL certificates for local HTTPS&lt;/li&gt;
&lt;li&gt;Command-line tools for common development tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using DDEV with Cloudflare Tunnel lets you run your site locally and share it publicly whenever needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we begin, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Mac, Linux, or Windows machine with Docker installed&lt;/li&gt;
&lt;li&gt;DDEV installed and configured (&lt;a href="https://ddev.readthedocs.io/en/stable/users/install/" rel="noopener noreferrer"&gt;official installation guide&lt;/a&gt; or check out my &lt;a href="https://dev.to/blog/how-to-setup-a-wordpress-and-drupal-local-site-with-ddev"&gt;guide on setting up DDEV for WordPress and Drupal&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;A working DDEV project (we'll use example sites like &lt;code&gt;myproject.ddev.site&lt;/code&gt; and &lt;code&gt;client-site.ddev.site&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Basic command-line familiarity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Cloudflare account required&lt;/strong&gt; for quick tunnels (the method covered in this guide)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you want persistent URLs with custom domains (covered later), you'll need a free Cloudflare account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Install Cloudflared
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;cloudflared&lt;/code&gt; daemon is the client that creates the tunnel connection. Installation varies by platform:&lt;/p&gt;

&lt;h3&gt;
  
  
  macOS (Homebrew)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew install cloudflared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Linux (Debian/Ubuntu)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.debsudo dpkg -i cloudflared-linux-amd64.deb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Linux (RHEL/CentOS)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-x86_64.rpmsudo rpm -i cloudflared-linux-x86_64.rpm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Windows
&lt;/h3&gt;

&lt;p&gt;Download the installer from the &lt;a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/" rel="noopener noreferrer"&gt;Cloudflare downloads page&lt;/a&gt; or use Chocolatey:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;choco install cloudflared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloudflared --version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Start Your DDEV Site
&lt;/h2&gt;

&lt;p&gt;Make sure your DDEV project is running. Navigate to your project directory and start DDEV:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd /path/to/your/projectddev start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your site will be available locally at something like &lt;code&gt;https://yourproject.ddev.site&lt;/code&gt;. Verify it works by opening it in your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Create a Cloudflare Tunnel to Your DDEV Site
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. Run one command and cloudflared creates a tunnel with a public URL automatically. &lt;strong&gt;No account setup, no authentication, no configuration needed&lt;/strong&gt;. The basic syntax is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloudflared tunnel --url &amp;lt;local-url&amp;gt; --http-host-header &amp;lt;hostname&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example 1: My Project Site
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloudflared tunnel --url https://myproject.ddev.site/ --http-host-header myproject.ddev.site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example 2: Client Site
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloudflared tunnel --url https://client-site.ddev.site/ --http-host-header client-site.ddev.site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After running this command, you'll see output like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2025-10-14T10:30:15Z INF Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee. If you intend to use Tunnels in production you should use a pre-created named tunnel by following: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps2025-10-14T10:30:15Z INF Requesting new quick Tunnel on trycloudflare.com...2025-10-14T10:30:16Z INF +--------------------------------------------------------------------------------------------+2025-10-14T10:30:16Z INF | Your quick Tunnel has been created! Visit it at (it may take some time to be reachable): |2025-10-14T10:30:16Z INF | https://randomly-generated-subdomain.trycloudflare.com |2025-10-14T10:30:16Z INF +--------------------------------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the generated URL (e.g., &lt;code&gt;https://randomly-generated-subdomain.trycloudflare.com&lt;/code&gt;) and share it with anyone who needs access to your local site!&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Command Parameters
&lt;/h2&gt;

&lt;p&gt;Let's break down what each parameter does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--url&lt;/code&gt;&lt;/strong&gt; : The local URL where your DDEV site is running. Use HTTPS if your DDEV site uses SSL (which it does by default).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--http-host-header&lt;/code&gt;&lt;/strong&gt; : This is crucial for DDEV. It tells cloudflared to forward the correct hostname in the HTTP Host header. DDEV routes requests based on this header, so without it, you'll get a "404 Not Found" or see the wrong site.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why is --http-host-header Necessary?
&lt;/h2&gt;

&lt;p&gt;DDEV's router uses virtual hosting, meaning multiple sites can run on the same IP address and port. The router determines which site to serve based on the &lt;code&gt;Host&lt;/code&gt; header in the HTTP request.&lt;/p&gt;

&lt;p&gt;When traffic comes through Cloudflare Tunnel, the Host header would normally be the Cloudflare-generated domain (like &lt;code&gt;randomly-generated-subdomain.trycloudflare.com&lt;/code&gt;). By specifying &lt;code&gt;--http-host-header myproject.ddev.site&lt;/code&gt;, we override this and ensure DDEV sees the correct hostname.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Named Tunnels for Persistent URLs
&lt;/h2&gt;

&lt;p&gt;The quick tunnel method above is perfect for ad-hoc sharing, but the URL changes each time you run the command and there's no uptime guarantee. If you need a persistent URL with your own custom domain (like &lt;code&gt;myproject.example.com&lt;/code&gt;), you can create a named tunnel. &lt;strong&gt;This requires a free Cloudflare account.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Authenticate with Cloudflare
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloudflared tunnel login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens a browser to authenticate with your Cloudflare account.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create a Named Tunnel
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloudflared tunnel create my-ddev-tunnel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates a tunnel ID and credentials file.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Create a Configuration File
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;~/.cloudflared/config.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tunnel: &amp;lt;TUNNEL-ID&amp;gt;credentials-file: /Users/yourusername/.cloudflared/&amp;lt;TUNNEL-ID&amp;gt;.jsoningress: - hostname: mysite.example.com service: https://myproject.ddev.site originRequest: httpHostHeader: myproject.ddev.site noTLSVerify: true - service: http_status:404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: &lt;code&gt;noTLSVerify: true&lt;/code&gt; is needed because DDEV uses self-signed certificates.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Route Your Domain
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloudflared tunnel route dns my-ddev-tunnel mysite.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Run the Tunnel
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloudflared tunnel run my-ddev-tunnel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your site is now accessible at &lt;code&gt;https://mysite.example.com&lt;/code&gt; with a persistent URL!&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Client Demos
&lt;/h3&gt;

&lt;p&gt;Share work-in-progress sites with clients without deploying to staging servers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloudflared tunnel --url https://client-demo.ddev.site/ --http-host-header client-demo.ddev.site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send the generated URL to your client for instant feedback.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Webhook Testing
&lt;/h3&gt;

&lt;p&gt;Test webhooks from services like Stripe, GitHub, or Twilio that require a public URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloudflared tunnel --url https://webhooks.ddev.site/ --http-host-header webhooks.ddev.site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure the webhook in the external service to point to your Cloudflare URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Mobile Device Testing
&lt;/h3&gt;

&lt;p&gt;Test your responsive designs on real mobile devices without being on the same network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloudflared tunnel --url https://mobile-test.ddev.site/ --http-host-header mobile-test.ddev.site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the Cloudflare URL on your phone to test on real devices.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Remote Collaboration
&lt;/h3&gt;

&lt;p&gt;Share your development environment with remote teammates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloudflared tunnel --url https://team-collab.ddev.site/ --http-host-header team-collab.ddev.site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your team can access the site as if they were running it locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Getting a 404 or Wrong Site
&lt;/h3&gt;

&lt;p&gt;Make sure you're using the &lt;code&gt;--http-host-header&lt;/code&gt; parameter with the correct DDEV hostname:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloudflared tunnel --url https://mysite.ddev.site/ --http-host-header mysite.ddev.site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SSL Certificate Errors
&lt;/h3&gt;

&lt;p&gt;If you're using a named tunnel with a config file, add &lt;code&gt;noTLSVerify: true&lt;/code&gt; to the origin request section since DDEV uses self-signed certificates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tunnel Not Starting
&lt;/h3&gt;

&lt;p&gt;Check if cloudflared is already running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ps aux | grep cloudflared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kill any existing processes if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pkill cloudflared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  DDEV Site Not Accessible Locally
&lt;/h3&gt;

&lt;p&gt;Verify your DDEV site is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ddev describe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure the URL you're using matches the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Considerations
&lt;/h2&gt;

&lt;p&gt;While Cloudflare Tunnel is secure by design, keep these points in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't expose production databases&lt;/strong&gt; - Only tunnel development sites with non-sensitive data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick tunnels are temporary&lt;/strong&gt; - URLs expire and shouldn't be relied upon for production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use authentication&lt;/strong&gt; - For sensitive projects, add Cloudflare Access authentication to your named tunnels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor tunnel access&lt;/strong&gt; - Check Cloudflare Analytics to see who's accessing your tunnels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shut down tunnels when done&lt;/strong&gt; - Use Ctrl+C to stop the tunnel when you're finished&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Alternative: DDEV's Built-in Share Command
&lt;/h2&gt;

&lt;p&gt;DDEV also has a built-in &lt;code&gt;ddev share&lt;/code&gt; command that uses &lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt;. However, Cloudflare Tunnel offers several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free without rate limits (&lt;a href="https://ngrok.com/pricing" rel="noopener noreferrer"&gt;ngrok free tier&lt;/a&gt; has limits)&lt;/li&gt;
&lt;li&gt;Better performance through Cloudflare's global network&lt;/li&gt;
&lt;li&gt;Integration with Cloudflare Zero Trust for advanced features&lt;/li&gt;
&lt;li&gt;Persistent named tunnels with custom domains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That said, &lt;code&gt;ddev share&lt;/code&gt; is even simpler if you just need something quick and don't want to install additional tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/" rel="noopener noreferrer"&gt;Cloudflare Tunnel Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ddev.readthedocs.io/" rel="noopener noreferrer"&gt;DDEV Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/" rel="noopener noreferrer"&gt;Creating Named Tunnels&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/cloudflare-one/identity/" rel="noopener noreferrer"&gt;Cloudflare Access for Authentication&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>webdev</category>
      <category>cloudinfrastructure</category>
    </item>
    <item>
      <title>How to use AI directly on your Kindle</title>
      <dc:creator>David Loor</dc:creator>
      <pubDate>Sat, 04 Oct 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/davo20019/how-to-use-ai-directly-on-your-kindle-3cj6</link>
      <guid>https://dev.to/davo20019/how-to-use-ai-directly-on-your-kindle-3cj6</guid>
      <description>&lt;p&gt;It keeps happening. I'm reading on my Kindle, come across something I don't fully understand, and face the same annoying choice: put the Kindle down and grab my phone, or struggle with the Kindle's browser trying to use ChatGPT's interface that wasn't built for e-ink screens.&lt;/p&gt;

&lt;p&gt;After dealing with this enough times, I spent this weekend building &lt;a href="https://www.kindle-chatgpt.com/" rel="noopener noreferrer"&gt;Kindle-ChatGPT&lt;/a&gt;: a simple AI chat that works directly in your Kindle's browser. No app to download, no account to create. Just type your question and get an answer optimized for e-ink.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with AI on Kindle
&lt;/h2&gt;

&lt;p&gt;When I'm reading on my Kindle and come across something I want to understand better, I have two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Put down the Kindle and grab my phone&lt;/li&gt;
&lt;li&gt;Try to use the Kindle's browser to access a traditional AI chat interface that's not optimized for e-ink displays&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither option is ideal. The first interrupts my reading flow, and the second gives me an interface that's painful to use on an e-ink screen with its low refresh rate and limited interactivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;Kindle-ChatGPT is a web app designed exclusively for Kindle e-reader browsers. Here's what makes it work well on Kindle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High contrast design&lt;/strong&gt; : Black and white interface optimized for e-ink displays&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple interaction&lt;/strong&gt; : Minimal UI that works with Kindle's browser limitations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No login required&lt;/strong&gt; : Access it instantly without creating an account&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight&lt;/strong&gt; : Fast loading and minimal battery drain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming responses&lt;/strong&gt; : See the AI's answer appear progressively, just like ChatGPT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The name says "ChatGPT" because that's what people search for, but it actually uses Google's Gemini API under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical implementation
&lt;/h2&gt;

&lt;p&gt;I built Kindle-ChatGPT with these technologies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js 15&lt;/a&gt;&lt;/strong&gt;: React framework with server-side rendering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.typescriptlang.org/" rel="noopener noreferrer"&gt;TypeScript&lt;/a&gt;&lt;/strong&gt;: Type safety for better code quality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind CSS&lt;/a&gt;&lt;/strong&gt;: Utility-first CSS for rapid UI development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://ai.google.dev/gemini-api/docs" rel="noopener noreferrer"&gt;Google Gemini AI&lt;/a&gt;&lt;/strong&gt;: Specifically the gemini-2.5-flash-lite-preview-09-2025 model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://workers.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare Workers&lt;/a&gt;&lt;/strong&gt;: Edge computing for fast global performance&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why Gemini instead of ChatGPT?
&lt;/h3&gt;

&lt;p&gt;While the service is named "ChatGPT" for discoverability, I chose Google's Gemini API for several technical reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Better free tier&lt;/strong&gt; : Gemini offers more generous rate limits for free usage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster responses&lt;/strong&gt; : The flash-lite model is optimized for speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native streaming&lt;/strong&gt; : Built-in SSE (Server-Sent Events) support for progressive responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lower latency&lt;/strong&gt; : Works well with Cloudflare's edge network&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Key technical challenges
&lt;/h3&gt;

&lt;p&gt;Building for Kindle presented unique challenges:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Kindle browser detection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The app only works on Kindle browsers to maintain focus on the optimized experience. I implemented device detection to ensure users get the interface designed specifically for e-ink displays.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Rate limiting without authentication&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since there's no login, I implemented IP-based rate limiting using &lt;a href="https://developers.cloudflare.com/kv/" rel="noopener noreferrer"&gt;Cloudflare KV&lt;/a&gt; storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Rate limiting configurationconst RATE_LIMIT_PER_MINUTE = 10; // Max 10 requests per minuteconst DAILY_MESSAGE_LIMIT = 100; // Max 100 messages per dayconst MAX_MESSAGE_LENGTH = 5000; // Max 5000 characters per message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents abuse while keeping the service free and accessible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Streaming responses for e-ink&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;E-ink displays have slow refresh rates, so I needed to balance streaming speed with readability. The implementation uses &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events" rel="noopener noreferrer"&gt;Server-Sent Events (SSE)&lt;/a&gt; to stream responses from Gemini:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:streamGenerateContent?alt=sse&amp;amp;key=${GEMINI_API_KEY}`;const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents, generationConfig: { temperature: 0.7, maxOutputTokens: 2048, topP: 0.95, topK: 40, }, }),});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Conversation history management&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To provide context-aware responses, I maintain conversation history on the client side and send it with each request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const contents = (history || []).map((msg: { role: string; content: string }) =&amp;gt; ({ role: msg.role === 'assistant' ? 'model' : 'user', parts: [{ text: msg.content.substring(0, MAX_MESSAGE_LENGTH) }],}));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to use it
&lt;/h2&gt;

&lt;p&gt;Using Kindle-ChatGPT is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your Kindle's web browser (Menu → Experimental Browser or Web Browser, depending on your model)&lt;/li&gt;
&lt;li&gt;Navigate to &lt;a href="https://www.kindle-chatgpt.com/" rel="noopener noreferrer"&gt;kindle-chatgpt.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Start typing your question in the text area&lt;/li&gt;
&lt;li&gt;Press Enter or tap the Send button&lt;/li&gt;
&lt;li&gt;Watch the response stream in&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Real-world use cases
&lt;/h2&gt;

&lt;p&gt;Here's how I actually use it while reading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quick definitions&lt;/strong&gt; : "What does 'epistemology' mean in simple terms?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context about historical events&lt;/strong&gt; : "What was happening in Europe in 1848?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concept clarification&lt;/strong&gt; : "Explain quantum entanglement like I'm 12"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Author background&lt;/strong&gt; : "Who is Yuval Noah Harari and what's his background?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Book recommendations&lt;/strong&gt; : "What other books are similar to Sapiens?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing help&lt;/strong&gt; : "Help me rephrase this sentence to be clearer"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is that I never have to leave my Kindle. The conversation stays in context, and the high-contrast interface doesn't strain my eyes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Kindle-only?
&lt;/h2&gt;

&lt;p&gt;Some people asked why I restricted it to Kindle browsers. Here's my reasoning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Focused optimization&lt;/strong&gt; : By targeting one device type, I can optimize the entire experience&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear value proposition&lt;/strong&gt; : Kindle users know exactly what they're getting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevents misuse&lt;/strong&gt; : Limits the potential for bot traffic and abuse&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Battery efficiency&lt;/strong&gt; : The simplified UI is designed specifically for e-ink's power characteristics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If someone tries to access the site from a regular browser, they see a landing page explaining the service and encouraging them to use it on their Kindle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and privacy
&lt;/h2&gt;

&lt;p&gt;Since there's no authentication, privacy was a top concern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No data storage&lt;/strong&gt; : Conversations aren't saved on the server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No tracking&lt;/strong&gt; : No analytics or cookies beyond what's necessary for rate limiting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP-based rate limiting&lt;/strong&gt; : Uses Cloudflare KV with automatic expiration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input validation&lt;/strong&gt; : Strict message length limits and content validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security headers&lt;/strong&gt; : Proper CSP, X-Frame-Options, and other security headers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deployment on Cloudflare
&lt;/h2&gt;

&lt;p&gt;I deployed this on Cloudflare Workers using &lt;a href="https://opennext.js.org/cloudflare" rel="noopener noreferrer"&gt;&lt;code&gt;@opennextjs/cloudflare&lt;/code&gt;&lt;/a&gt;, which adapts Next.js for Cloudflare's edge network. This gives several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Global edge network&lt;/strong&gt; : Fast response times worldwide&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tier&lt;/strong&gt; : 100,000 requests per day on the free plan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KV storage&lt;/strong&gt; : Built-in key-value storage for rate limiting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic scaling&lt;/strong&gt; : Handles traffic spikes without configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The build process is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run pages:buildnpm run deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;Building this taught me several things about working with constrained environments:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Constraints can simplify decisions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Kindle's limitations (slow refresh, limited JavaScript, basic CSS support) forced me to strip away unnecessary complexity. What I ended up with was simpler and more focused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Progressive enhancement matters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The app works with minimal JavaScript. If streaming fails, it falls back to a simple request-response model. This makes it more resilient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Building for yourself speeds up iteration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I built this because I needed it. Every technical decision was tested immediately by actually using the app on my Kindle while reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Free services need clear limits&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without proper rate limiting, a free AI service would be abused instantly. The 100 messages per day limit is generous enough for legitimate use but prevents abuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;I'm considering these additions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Support for multiple AI models (letting users choose between Gemini, Claude, etc.)&lt;/li&gt;
&lt;li&gt;Saved conversation history (optional, with user consent)&lt;/li&gt;
&lt;li&gt;Integration with Kindle's built-in dictionary&lt;/li&gt;
&lt;li&gt;Export conversations to your email&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But I'm being cautious about adding features. The simplicity is part of what makes it work well on Kindle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;If you have a Kindle e-reader, try it out at &lt;a href="https://www.kindle-chatgpt.com/" rel="noopener noreferrer"&gt;kindle-chatgpt.com&lt;/a&gt;. It's free and requires no signup.&lt;/p&gt;

</description>
      <category>aitools</category>
      <category>webdev</category>
      <category>sideprojects</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>How to read Nginx access logs</title>
      <dc:creator>David Loor</dc:creator>
      <pubDate>Wed, 20 Aug 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/davo20019/how-to-read-nginx-access-logs-44cd</link>
      <guid>https://dev.to/davo20019/how-to-read-nginx-access-logs-44cd</guid>
      <description>&lt;p&gt;When something breaks, the access log tells a story. Who hit the site. What they asked for. Which URLs failed. When a spike started. I used these steps on a real project and they worked. The examples below are safe to copy and run, then tweak for your setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are Nginx access logs?
&lt;/h2&gt;

&lt;p&gt;Nginx access logs are text files that record every HTTP request your web server receives. Each line represents one request and contains details like the client's IP address, timestamp, HTTP method, URL path, status code, response size, and more.&lt;/p&gt;

&lt;p&gt;These logs are automatically generated by Nginx as it processes requests. Every time someone visits your site, clicks a link, submits a form, or even hits a broken URL, Nginx writes a line to the access log. This happens in real-time, so the log file is constantly growing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How access logs work
&lt;/h2&gt;

&lt;p&gt;Nginx uses a configurable log format defined in your nginx.conf file. The default format looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                '$status $body_bytes_sent "$http_referer" '
                '"$http_user_agent"';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates log entries like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;192.168.1.100 - - [20/Aug/2025:10:30:45 +0000] "GET /blog/post HTTP/1.1" 200 1234 "https://example.com/" "Mozilla/5.0..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Many modern setups use a more detailed JSON-like format with key=value pairs, which is what the examples in this post assume.&lt;/p&gt;

&lt;h2&gt;
  
  
  CDN vs. Origin: Where to look for logs
&lt;/h2&gt;

&lt;p&gt;If you're using a CDN (like Cloudflare, CloudFront, or Fastly), there are two different sets of logs to consider:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CDN edge logs&lt;/strong&gt; : Show requests from actual users hitting the CDN. These contain the real client IPs, user agents, and geographic locations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Origin server logs&lt;/strong&gt; (what this post covers): Show requests that made it through the CDN to your Nginx server. These logs often show the CDN's IP address as the client, not the end user's IP.&lt;/p&gt;

&lt;p&gt;For troubleshooting, you usually want the CDN logs first. Only check origin logs when you need to see what actually reached your server or when debugging server-side issues.&lt;/p&gt;

&lt;p&gt;All examples assume key=value logs (fields like status="404" and request="GET /path HTTP/1.1"), and files under /var/log/nginx/. Adjust names as needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will learn
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How to read live and gzipped logs in one go&lt;/li&gt;
&lt;li&gt;How to find top IPs and top failing URLs&lt;/li&gt;
&lt;li&gt;How to pull everything from one IP&lt;/li&gt;
&lt;li&gt;How to zoom in on a short time window&lt;/li&gt;
&lt;li&gt;How to tie access logs to WAF blocks&lt;/li&gt;
&lt;li&gt;How to keep results fast and useful&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;

&lt;p&gt;Read everything, no matter if the file is .log or .gz:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;zgrep -h . /var/log/nginx/ssl-*.access.log*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That zgrep trick handles both normal and rotated files. Add filters after it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools you'll use
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;zgrep&lt;/strong&gt; : Like grep but works on both regular files and compressed (.gz) files. Perfect for log files that get rotated and compressed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;awk&lt;/strong&gt; : A powerful text processing tool that can split lines by delimiters and extract specific fields. Great for parsing structured log formats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;cut&lt;/strong&gt; : Extracts specific columns or fields from text. Use -d to specify a delimiter (like quotes or commas) and -f to pick which field number you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find top IPs behind 404 storms
&lt;/h2&gt;

&lt;p&gt;When editors report "lots of broken links," start here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;zgrep -h . /var/log/nginx/ssl-*.access.log* \
| awk -F'status="' '$2 ~ /^404"/' \
| awk -F'x_forwarded_for="' '{print $2}' \
| cut -d'"' -f1 | cut -d',' -f1 \
| LC_ALL=C sort | uniq -c | sort -nr | head
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why it helps. You see the worst offenders first. If an IP is scanning random paths, you will spot it fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  See everything a single IP did
&lt;/h2&gt;

&lt;p&gt;Handy when you need to explain a block or a spike from one source.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IP="112.134.209.112"
zgrep -h . /var/log/nginx/ssl-*.access.log* \
| grep -F 'x_forwarded_for="'$IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tip. In some stacks the client IP can live in src or src_ip. If so, swap the field name in the grep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Top URLs hit by that IP (ignore query strings)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IP="112.134.209.112"
zgrep -h . /var/log/nginx/ssl-*.access.log* \
| grep -F 'x_forwarded_for="'$IP \
| awk -F'request="' '{print $2}' | cut -d'"' -f1 \
| awk '{print $2}' | cut -d'?' -f1 \
| LC_ALL=C sort | uniq -c | sort -nr | head
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why it helps. Dropping the query string groups "the same page with different params" together, which makes patterns obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Top failing URLs by status
&lt;/h2&gt;

&lt;p&gt;404s:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;zgrep -h . /var/log/nginx/ssl-*.access.log* \
| awk -F'status="' '$2 ~ /^404"/' \
| awk -F'request="' '{print $2}' | cut -d'"' -f1 \
| awk '{print $2}' | cut -d'?' -f1 \
| LC_ALL=C sort | uniq -c | sort -nr | head
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5xx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;zgrep -h . /var/log/nginx/ssl-*.access.log* \
| awk -F'status="' '{split($2,a,"\""); if (a[1] ~ /^5[0-9][0-9]$/) print}' \
| awk -F'request="' '{print $2}' | cut -d'"' -f1 \
| awk '{print $2}' | cut -d'?' -f1 \
| LC_ALL=C sort | uniq -c | sort -nr | head
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why it helps. This gives you a ranked list of problem paths. Fix the top five and you often fix most of the pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zoom in on a short time window
&lt;/h2&gt;

&lt;p&gt;When a spike hits at a known time, you want before and after.&lt;/p&gt;

&lt;p&gt;Simple and quick (good enough for a 10-minute window inside the same hour):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Example: 09/Aug/2025 23:15–23:25
zgrep -h 'time_local="' /var/log/nginx/ssl-*.access.log* \
| egrep '09/Aug/2025:23:1[5-9]|09/Aug/2025:23:2[0-5]'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pipe the result into any of the counts above.&lt;/p&gt;

&lt;p&gt;Why it helps. You compare traffic right before and right after an event, which is often all you need to see what changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Form posts and large bodies
&lt;/h2&gt;

&lt;p&gt;Large POSTs to a form can trip WAF rules. Measure them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PATH_RE="/about/.*speaker-request-form"
zgrep -h . /var/log/nginx/ssl-*.access.log* \
| awk -F'request="' -v r="$PATH_RE" '
  { split($2,a,"\""); split(a[1],b," "); m=b[1]; u=b[2];
    if (m=="POST" &amp;amp;&amp;amp; u ~ r) print }' \
| awk -F'request_length="' '{print $2}' | cut -d'"' -f1 \
| awk '{sum+=$1; c++} END{print "POST count="c, "avg_request_length=" (c?sum/c:0)}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why it helps. If average request size is high, your WAF may block inspection. You now have proof and numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tie access logs to WAF blocks
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Grab the exact time and rule from your WAF event.&lt;/li&gt;
&lt;li&gt;Filter access logs for that window using the "time window" trick.&lt;/li&gt;
&lt;li&gt;Look for the same path, the same IP, or a large request_length.&lt;/li&gt;
&lt;li&gt;If the WAF rule talks about body size or inspection limits, confirm with the POST analysis above.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This closes the loop. You can say what happened and why it happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed tips that matter
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Filter early. Add grep 'status="404"' before you sort.&lt;/li&gt;
&lt;li&gt;Use LC_ALL=C sort for faster and stable sorting.&lt;/li&gt;
&lt;li&gt;Drop query strings to group pages by path.&lt;/li&gt;
&lt;li&gt;Sample first. Run head on a pipeline to check you are slicing the right field.&lt;/li&gt;
&lt;li&gt;Document your log_format in the repo. Future you will thank you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Counting by remote_addr when your app sits behind a proxy. Use x_forwarded_for or your real client IP field.&lt;/li&gt;
&lt;li&gt;Assuming all log files use the same format. Check your nginx.conf log_format before parsing.&lt;/li&gt;
&lt;li&gt;Forgetting that rotated logs (.gz files) need zgrep, not grep.&lt;/li&gt;
&lt;li&gt;Using complex regex patterns that break when log formats change. Keep it simple.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;Logs answer real questions fast. Start with a clear question, pipe only what you need, and count. Tie what you see in Nginx to what your WAF reports. You will move from "something felt slow" to "this URL failed 2,379 times from one IP in ten minutes, and here is the fix."&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>devops</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>Why I Chose Next.js with AI-Powered Tools Over WordPress to Rebuild My Website</title>
      <dc:creator>David Loor</dc:creator>
      <pubDate>Wed, 12 Feb 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/davo20019/why-i-chose-nextjs-with-ai-powered-tools-over-wordpress-to-rebuild-my-website-33p2</link>
      <guid>https://dev.to/davo20019/why-i-chose-nextjs-with-ai-powered-tools-over-wordpress-to-rebuild-my-website-33p2</guid>
      <description>&lt;p&gt;I wanted to redesign my website in one weekend and considered three possible tools: &lt;a href="https://www.drupal.org/" rel="noopener noreferrer"&gt;Drupal&lt;/a&gt;, &lt;a href="https://wordpress.org/" rel="noopener noreferrer"&gt;WordPress&lt;/a&gt;, and &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt;. Since my existing site was built on WordPress, it seemed like the best choice. With WordPress, I would not need to migrate content, my server was already set up, and all I needed to do was apply a new theme.&lt;/p&gt;

&lt;p&gt;However, I chose Next.js with AI-powered tools to redesign my website. Here's why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Redesign Process
&lt;/h2&gt;

&lt;p&gt;A website redesign is more than just launching a new version. The process depends on how complex the project is and what the goals are. Some redesigns are simple updates, while others require rebuilding the entire site, including its layout, design, and features.&lt;/p&gt;

&lt;p&gt;Key parts of a redesign may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating a new design or improving the existing one&lt;/li&gt;
&lt;li&gt;Changing the content layout to make it easier to read and use&lt;/li&gt;
&lt;li&gt;Improving SEO by making the site faster and easier to find on search engines&lt;/li&gt;
&lt;li&gt;Adding new features based on what users need&lt;/li&gt;
&lt;li&gt;Setting up redirects so old URLs still work and don't hurt search rankings&lt;/li&gt;
&lt;li&gt;Making the site more accessible and faster for a better user experience&lt;/li&gt;
&lt;li&gt;Moving content or restructuring data if switching platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My redesign project would need to address all of these aspects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Challenges
&lt;/h2&gt;

&lt;p&gt;I knew that I lacked design skills. More importantly, I don't enjoy the design process. I searched for WordPress themes that matched my vision but couldn't find any. This meant I would have to create a custom theme or page builder templates if I stayed with WordPress. The same issue applied to Drupal because nothing fit my needs.&lt;/p&gt;

&lt;p&gt;Since I'm not a designer, I had two choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use an existing theme&lt;/li&gt;
&lt;li&gt;Get help&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I decided to use AI-powered tools to bring my ideas to life. With just a few descriptive prompts, the AI created all the UI components I needed from navigation menus to content layouts and responsive designs.&lt;/p&gt;

&lt;p&gt;From the backend (admin area), Drupal and WordPress do not yet fully support AI-powered tools for generating custom layouts, or they do not work optimally. The available options still require logging into the site and repeatedly clicking through multiple steps. However, the Drupal community is making significant progress in this area, and I plan to write about it soon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Chose Next.js
&lt;/h2&gt;

&lt;p&gt;I ruled out WordPress and Drupal because they required too much manual work. Despite my Drupal experience, setting up a site with modules and themes would take more time than I had. With only a weekend to complete the project, I needed a simple solution without unnecessary complexity.&lt;/p&gt;

&lt;p&gt;Instead, I turned to Next.js with modern development tools. &lt;a href="https://cursor.sh/" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt; and &lt;a href="https://codeium.com/" rel="noopener noreferrer"&gt;Windsurf&lt;/a&gt;, both &lt;a href="https://en.wikipedia.org/wiki/Integrated_development_environment" rel="noopener noreferrer"&gt;IDEs&lt;/a&gt;, simplify UI design by generating structured, production ready code from simple prompts. You can describe what you want in plain English, like "create a responsive navigation menu with a logo and links," and get deployable code instantly. I've been using these tools for personal projects and work for quite some time now.&lt;/p&gt;

&lt;p&gt;One of the keys to efficient AI-powered development is crafting good prompts. For the migration process, I provided the URLs of my existing WordPress pages and described the desired Next.js content structure. With these detailed prompts, Cursor generated scripts to scrape the content from my old site and transform it into the new format. The same approach helped create optimized URLs and redirect rules to maintain SEO rankings, while ensuring proper metadata and semantic HTML structure throughout the site.&lt;/p&gt;

&lt;p&gt;Traditionally, you'd write every line of code manually. These IDEs generate entire components and pages through simple prompts, following best practices and maintaining code quality. The generated code can be immediately deployed to your server.&lt;/p&gt;

&lt;p&gt;To streamline the process, I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://chat.openai.com/" rel="noopener noreferrer"&gt;ChatGPT&lt;/a&gt;, &lt;a href="https://gemini.google.com/" rel="noopener noreferrer"&gt;Gemini&lt;/a&gt;, and &lt;a href="https://claude.ai/" rel="noopener noreferrer"&gt;Claude&lt;/a&gt; for brainstorming and crafting effective prompts&lt;/li&gt;
&lt;li&gt;Cursor and Windsurf for code generation and structure&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind CSS&lt;/a&gt; for rapid styling and responsive design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tailwind enabled fast styling with consistent, optimized results.&lt;/p&gt;

&lt;p&gt;As a bonus, hosting on Vercel's free tier saves me $5 monthly in hosting costs and eliminates hours spent on WordPress updates and server maintenance every month.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Power of AI
&lt;/h2&gt;

&lt;p&gt;AI enables us to achieve things we previously could not, whether due to lack of expertise or lack of interest.&lt;/p&gt;

&lt;p&gt;Another major factor in my decision was speed.&lt;/p&gt;

&lt;p&gt;My old WordPress site scored only 65 on PageSpeed Insights for mobile devices, as evidenced by a last performance test:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36l6n0a3ae93ds75x8mm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36l6n0a3ae93ds75x8mm.png" alt="PageSpeed Insights showing a performance score of 65 for the old WordPress site on mobile devices" width="800" height="579"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;PageSpeed Insights results for the old WordPress site showing a mobile performance score of 65&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In contrast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js is incredibly fast, scoring 100 on PageSpeed for both mobile and desktop&lt;/li&gt;
&lt;li&gt;Achieving this took only a few well crafted prompts and just a few minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Embracing AI While Maintaining Control
&lt;/h2&gt;

&lt;p&gt;AI is revolutionizing software development by dramatically increasing productivity. It allows developers to accomplish tasks that previously required entire teams or seemed out of reach due to time constraints, lack of expertise, or limited interest in certain aspects of development.&lt;/p&gt;

&lt;p&gt;However, it's crucial to understand that AI is a powerful assistant, not a replacement for fundamental knowledge and critical thinking. Here are some key insights I've gained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Learning the fundamentals is more important than ever. You need to understand what the AI is doing and why&lt;/li&gt;
&lt;li&gt;Position yourself as the driver, with AI as your assistant handling repetitive and manual tasks&lt;/li&gt;
&lt;li&gt;Focus your energy on the challenging aspects: understanding user needs, system design, architecture decisions, and problem solving&lt;/li&gt;
&lt;li&gt;Strong communication skills like active listening have become even more crucial in the AI era&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is to embrace AI while maintaining control over your development process. This means reviewing generated code thoroughly, understanding its implications, and ensuring it aligns with your project requirements and best practices.&lt;/p&gt;

&lt;p&gt;In my case, AI allowed me to turn my ideas into reality with a few well crafted prompts. It helped me iterate quickly, experiment, and refine until I got a result I liked, all at a much lower cost in time.&lt;/p&gt;

&lt;p&gt;While I do love Drupal and WordPress, and they're great tools for many use cases that I'll continue using where they make sense, AI-powered development with Next.js proved to be the perfect solution for this particular project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technology Stack
&lt;/h2&gt;

&lt;p&gt;Here's the complete stack I used to build this website:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Version Control &amp;amp; Hosting:&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/konstruktoid/konstruktoid-deleted-branch-renovatestep-security-harden-runner-2x-at-konstruktoidtymely-4g6h-temp-slug-892592"&gt;GitHub&lt;/a&gt; (Free tier) for version control and collaboration&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vercel.com/" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt; (Free tier) for hosting and automatic deployments&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt; (Free tier) for DNS management and security
|
| &lt;strong&gt;Frontend Framework &amp;amp; Styling:&lt;/strong&gt; | &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; (Open Source) as the core framework&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind CSS&lt;/a&gt; (Open Source) for styling and responsive design
|
| &lt;strong&gt;AI Development Tools:&lt;/strong&gt; | &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cursor.sh/" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt; and &lt;a href="https://codeium.com/" rel="noopener noreferrer"&gt;Windsurf&lt;/a&gt; for AI-powered development - using both allowed me to compare different layout outputs and choose the best ones
|
| &lt;strong&gt;Forms &amp;amp; Communication:&lt;/strong&gt; | &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.brevo.com/" rel="noopener noreferrer"&gt;Brevo&lt;/a&gt; (Free tier) for the calendar link and contact form, offering a generous 300 emails per day
|&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>ai</category>
    </item>
    <item>
      <title>Understanding Composer: A Simple Analogy of Grocery Shopping for Better Dependency Management</title>
      <dc:creator>David Loor</dc:creator>
      <pubDate>Sat, 06 May 2023 18:26:50 +0000</pubDate>
      <link>https://dev.to/davo20019/understanding-composer-a-simple-analogy-of-grocery-shopping-for-better-dependency-management-hh6</link>
      <guid>https://dev.to/davo20019/understanding-composer-a-simple-analogy-of-grocery-shopping-for-better-dependency-management-hh6</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F05%2Fdevelopeer-shopping-2.jpg%3Fresize%3D800%252C662%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F05%2Fdevelopeer-shopping-2.jpg%3Fresize%3D800%252C662%26ssl%3D1" alt="Developer shopping" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Whether you’re an application developer working in PHP or simply a tech enthusiast interested in the field, you’ve likely encountered &lt;a href="https://getcomposer.org/" rel="noopener noreferrer"&gt;Composer&lt;/a&gt;. It plays a significant role in managing dependencies for PHP projects. But in today’s blog post, I’ll take a different approach to understanding PHP’s Composer, by seeing it as a personal shopper who buys groceries for various recipes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The composer.json – Our Custom Grocery List
&lt;/h2&gt;

&lt;p&gt;Think of the &lt;code&gt;composer.json&lt;/code&gt; file as a grocery list that outlines the essential items you need to make your favorite dish. In the realm of dependency management, this list includes the names and desired quantities (versions) of libraries and packages required for the successful functioning of your application recipe.&lt;/p&gt;

&lt;p&gt;For example, imagine that you must prepare spaghetti with marinara sauce. You provide a personal shopper with a list containing pasta, tomato sauce, and Italian seasoning. Similarly, when crafting your &lt;code&gt;composer.json&lt;/code&gt; file, you specify the required packages, like Laravel and Guzzle, that form the building blocks of your software application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Composer Commands – Add, Update, or Remove Items From the List
&lt;/h2&gt;

&lt;p&gt;Sometimes, you might want to make changes to your grocery list – perhaps to update an item or remove it altogether. In the context of Composer, you can use commands such as &lt;a href="https://getcomposer.org/doc/03-cli.md#require-r" rel="noopener noreferrer"&gt;&lt;code&gt;composer require package-name&lt;/code&gt; &lt;/a&gt;and &lt;code&gt;composer remove package-name&lt;/code&gt; to instruct your personal shopper to add or remove items (libraries or packages) from the list (&lt;code&gt;composer.json&lt;/code&gt; file).&lt;/p&gt;

&lt;p&gt;For instance, if you suddenly decide to add mushrooms to your spaghetti marinara recipe, you can communicate with your personal shopper to include that item. Similarly, when you want to add a new package or update an existing one, you can use the respective composer commands to adjust the list quickly and efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The composer.lock – The Final Purchased List
&lt;/h2&gt;

&lt;p&gt;Once the personal shopper has perused the shopping aisles, they provide you with an itemized receipt that clearly states the brands and amounts of each ingredient purchased. This is the &lt;code&gt;composer.lock&lt;/code&gt; file – a precise representation of the libraries and packages in their specific versions that were procured to follow your grocery list.&lt;/p&gt;

&lt;p&gt;By sharing the &lt;code&gt;composer.lock&lt;/code&gt; file with your team members, you can be confident that everyone on the project uses the same, carefully selected groceries for the application recipe. This consistency is crucial when developing software, ensuring that all collaborators are working with the same library versions, which eliminates any potential discrepancies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This article explores dependency management by drawing a simple analogy between grocery shopping and dependency management in software development. By conceptualizing &lt;code&gt;composer.json&lt;/code&gt; and &lt;code&gt;composer.lock&lt;/code&gt; files as grocery lists, you can more easily grasp the inner workings of Composer and manage dependencies more effectively.&lt;/p&gt;

&lt;p&gt;While using a personal shopper to manage your groceries might not be necessary for whipping up quick meals at home, it becomes essential when running complex projects like hotels or restaurants, highlighting the importance of using the right tools in scenarios that demand them.&lt;/p&gt;

&lt;p&gt;Now it’s time to cook up some exceptional applications, knowing full well that you have the perfect ingredients on hand!&lt;/p&gt;

</description>
      <category>drupal</category>
      <category>composer</category>
      <category>php</category>
    </item>
    <item>
      <title>How to Use the Drupal Feeds and Feeds Tamper Modules for Easy CSV Data Imports</title>
      <dc:creator>David Loor</dc:creator>
      <pubDate>Sat, 29 Apr 2023 16:57:33 +0000</pubDate>
      <link>https://dev.to/davo20019/how-to-use-the-drupal-feeds-and-feeds-tamper-modules-for-easy-csv-data-imports-55h8</link>
      <guid>https://dev.to/davo20019/how-to-use-the-drupal-feeds-and-feeds-tamper-modules-for-easy-csv-data-imports-55h8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2Fdrupal-feeds-feeds-tamper-modules-banner.png%3Fresize%3D800%252C419%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2Fdrupal-feeds-feeds-tamper-modules-banner.png%3Fresize%3D800%252C419%26ssl%3D1" alt="Drupal feeds and feeds tamper modules" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Feeds module in Drupal 9 is a powerful tool for importing external data into your Drupal website. It supports a variety of formats, including CSV, XML, and JSON, and can import content into nodes, taxonomies, and other entities in your Drupal site. To enhance the importing process, you can use the Feeds Tamper module, which allows you to modify data before it is saved. This tutorial will walk you through the process of using the Feeds module along with Feeds Tamper to import data from a CSV file.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Required modules:

&lt;ul&gt;
&lt;li&gt;Install and enable the following modules:&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.drupal.org/project/feeds" rel="noopener noreferrer"&gt;Feeds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.drupal.org/project/feeds_tamper" rel="noopener noreferrer"&gt;Feeds Tamper&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Create a content type:

&lt;ul&gt;
&lt;li&gt;If you don’t already have one, create a content type for the data you want to import from the CSV file.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-9.03.08-AM.png%3Fresize%3D683%252C321%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-9.03.08-AM.png%3Fresize%3D683%252C321%26ssl%3D1" alt="Add content type" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set up a Feeds Importer:

&lt;ul&gt;
&lt;li&gt;Go to &lt;code&gt;Structure &amp;gt; Feeds types&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click on &lt;code&gt;Add feed type&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Enter a name and description for your importer&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2Fsave-feed-type-1.jpg%3Fresize%3D800%252C259%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2Fsave-feed-type-1.jpg%3Fresize%3D800%252C259%26ssl%3D1" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Create a new Feed Type&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-11.46.57-AM.png%3Fresize%3D800%252C303%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-11.46.57-AM.png%3Fresize%3D800%252C303%26ssl%3D1" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Basic settings for the new feed&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure the Fetcher: 

&lt;ul&gt;
&lt;li&gt;Click on the “Fetcher” dropdown, and choose how you’d like to fetch the file. For this tutorial, use &lt;code&gt;Upload File&lt;/code&gt; to manually upload the CSV file. Refer to the Fetcher a. and b. images below.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-9.16.19-AM.png%3Fresize%3D800%252C286%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-9.16.19-AM.png%3Fresize%3D800%252C286%26ssl%3D1" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fetcher a. Choose the Upload file fetcher&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-11.20.28-AM-1.png%3Fresize%3D800%252C404%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-11.20.28-AM-1.png%3Fresize%3D800%252C404%26ssl%3D1" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fetcher b. Fetcher settings&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure the &lt;strong&gt;Parser&lt;/strong&gt; : 

&lt;ul&gt;
&lt;li&gt;Click on the “Parser” dropdown, choose &lt;code&gt;CSV&lt;/code&gt;, and configure the appropriate CSV settings, like delimiter, No headers. The default settings will work for most of the use cases. Refer to the images below.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-9.16.06-AM.png%3Fresize%3D800%252C288%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-9.16.06-AM.png%3Fresize%3D800%252C288%26ssl%3D1" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Parser a. Choose the CSV Parser&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-9.29.15-AM.png%3Fresize%3D800%252C244%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-9.29.15-AM.png%3Fresize%3D800%252C244%26ssl%3D1" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Parser b. Parser settings&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure the Processor: Click on the “Processor” dropdown and choose Node. Then, click on the “Content Type” dropdown, and choose the target content type, and configure additional settings like author and default language. The Processor dropdown allows you to choose the type of entity that will be created when importing data. Refer to the images below.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-11.30.28-AM.png%3Fresize%3D800%252C514%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-11.30.28-AM.png%3Fresize%3D800%252C514%26ssl%3D1" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Processor a. Choose the processor&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-11.36.06-AM.png%3Fresize%3D800%252C499%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-11.36.06-AM.png%3Fresize%3D800%252C499%26ssl%3D1" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Processor b. Choose the Content Type&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-11.14.43-AM.png%3Fresize%3D800%252C425%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-11.14.43-AM.png%3Fresize%3D800%252C425%26ssl%3D1" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Processor b. Processor settings&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Language&lt;/strong&gt; : This setting allows you to select the language for the imported content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update existing content items&lt;/strong&gt; : This setting determines how existing content items are handled when new data is imported. You can choose to update existing content items, not update them, or replace them entirely. The module will use the unique mappings you have set up to determine which content items are considered “existing” and should be updated or replaced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previously imported items&lt;/strong&gt; : This setting allows you to choose what to do with items that were previously imported but are no longer present in the feed. You can choose to keep them or take some other action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expire content items&lt;/strong&gt; : This setting allows you to specify when content items should be deleted. You can choose to never delete them or set a specific time period after which they will be deleted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Owner&lt;/strong&gt; : This setting allows you to specify the owner of the imported content. You can choose to use the feed author as the owner or select a specific user. If you leave this field blank, the owner will be set as Anonymous.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Click on Save and add mappings&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Map the source CSV fields to the target content type fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click on the  &lt;strong&gt;Mapping&lt;/strong&gt;  tab: This will take you to the mapping configuration page where you can map source data to target fields.&lt;/li&gt;
&lt;li&gt;Map source CSV columns to the target content type fields by clicking  &lt;strong&gt;Select a target&lt;/strong&gt; : This will allow you to select the target Drupal field that you want to map data to. Then, in the “Source” column, select &lt;strong&gt;New CSV Source&lt;/strong&gt; and enter the name of the column from your CSV file that contains the data you want to map to the selected target field.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Optionally, configure unique fields to avoid duplicate imports: This allows you to specify which fields should be used to determine if an imported item is a duplicate of an existing item. If a duplicate is found, the module can be configured to update or replace the existing item.&lt;/li&gt;
&lt;li&gt;Tweak other configurations like language handling: This allows you to configure additional settings for your importer, such as how language should be handled.&lt;/li&gt;
&lt;li&gt;Click  &lt;strong&gt;Save&lt;/strong&gt; : This will save your mapping and other configuration settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-12.13.46-PM.png%3Fresize%3D800%252C415%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2FScreenshot-2023-04-29-at-12.13.46-PM.png%3Fresize%3D800%252C415%26ssl%3D1" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Map CSV fields to Drupal fields&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Import the data using the Feeds importer:

&lt;ul&gt;
&lt;li&gt;Go to &lt;code&gt;Content &amp;gt; Feeds importers&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click on &lt;code&gt;Import&lt;/code&gt; for the importer that you created previously&lt;/li&gt;
&lt;li&gt;Upload your CSV file (If you configured file upload as your fetcher)&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Import&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The imported content should now appear as nodes of the target content type within your Drupal site &lt;code&gt;/admin/content&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Use Feeds Tamper to manipulate imported data
&lt;/h2&gt;

&lt;p&gt;The Feeds Tamper module is a contrib module for the Drupal Feeds module that allows you to modify data before it is imported. It provides a user interface for adding “tamper” plugins to the individual mapped fields of your Feeds importer, which can perform various data manipulation tasks such as splitting, combining, or rewriting data.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm that the Feeds Tamper module is enabled.&lt;/li&gt;
&lt;li&gt;Go to the configuration page for your feed importer: Navigate to the configuration page for the feed importer that you want to use with the Feeds Tamper module. You can do this by going to the “Structure” page, then clicking on “Feeds” and selecting the importer you want to configure.&lt;/li&gt;
&lt;li&gt;Click on the “Tamper” tab: On the configuration page for your feed importer, click on the “Tamper” tab to access the tamper settings.&lt;/li&gt;
&lt;li&gt;Add tamper plugins to mapped fields: In the tamper settings table, find the mapped field to which you want to add a tamper plugin to. In the “Plugin” column for that field, click on the “Add plugin” button. Select the plugin you want to use from the list of available plugins, then configure its settings as needed.&lt;/li&gt;
&lt;li&gt;Save your changes: Once you have added and configured all of the tamper plugins you want to use, click on the “Save” button to save your changes.&lt;/li&gt;
&lt;li&gt;Repeat these steps for all the source columns you want to modify before storing the data&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Feeds tamper module’s use cases
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data cleanup&lt;/strong&gt; : You can use the Feeds Tamper module to clean up data before it is imported. For example, you can use the “Find and replace” plugin to replace specific words or phrases in your data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data transformation&lt;/strong&gt; : You can use the Feeds Tamper module to transform data before it is imported. For example, you can use the “Explode” plugin to split a single field into multiple values, or the “Implode” plugin to combine multiple fields into a single value. The “Explode” plugin is useful when mapping multiple comma-separated fields from the CSV file to a taxonomy reference field. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data filtering&lt;/strong&gt; : You can use the Feeds Tamper module to filter data before it is imported. For example, you can use the “Keyword Filter” plugin to only import items that contain specific keywords.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data rewriting&lt;/strong&gt; : You can use the Feeds Tamper module to rewrite data before it is imported. For example, you can use the “Rewrite” plugin to change the format of dates or numbers in your data, or the “Pathauto” plugin to automatically generate URL aliases for imported content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that’s it! You have successfully configured the Feeds module in Drupal 9 to import and manipulate data from a CSV file using the Feeds Tamper module.&lt;/p&gt;

</description>
      <category>drupal</category>
      <category>csvimport</category>
      <category>feedsmodule</category>
    </item>
    <item>
      <title>Creating Gutenberg Blocks with Advanced Custom Fields (ACF) and LazyBlocks: A Comparative Guide</title>
      <dc:creator>David Loor</dc:creator>
      <pubDate>Mon, 03 Apr 2023 00:01:52 +0000</pubDate>
      <link>https://dev.to/davo20019/creating-gutenberg-blocks-with-advanced-custom-fields-acf-and-lazyblocks-a-comparative-guide-1kn</link>
      <guid>https://dev.to/davo20019/creating-gutenberg-blocks-with-advanced-custom-fields-acf-and-lazyblocks-a-comparative-guide-1kn</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2Fgutenberg-wordpress.png%3Fresize%3D800%252C477%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F04%2Fgutenberg-wordpress.png%3Fresize%3D800%252C477%26ssl%3D1" alt="gutenberg editor" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gutenberg, the WordPress block editor, has revolutionized the way we create and design content in WordPress. &lt;a href="https://www.advancedcustomfields.com/" rel="noopener noreferrer"&gt;Advanced Custom Fields (ACF)&lt;/a&gt; and &lt;a href="https://wordpress.org/plugins/lazy-blocks/" rel="noopener noreferrer"&gt;LazyBlocks&lt;/a&gt; are two popular plugins that extend Gutenberg’s functionality by allowing users to create custom blocks. In this article, we’ll discuss the process of creating Gutenberg blocks with both plugins and analyze their pros and cons.&lt;/p&gt;

&lt;h2&gt;
  
  
  1.- Advanced Custom Fields (ACF)
&lt;/h2&gt;

&lt;p&gt;ACF is a popular WordPress plugin that allows users to add custom fields to their posts, pages, and custom post types. It also supports creating custom Gutenberg blocks using a user-friendly interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating Blocks with ACF:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install and activate the &lt;a href="https://wordpress.org/plugins/advanced-custom-fields/" rel="noopener noreferrer"&gt;ACF plugin&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Add the callback function &lt;code&gt;my_acf_block_render_callback()&lt;/code&gt; to your theme’s &lt;code&gt;functions.php&lt;/code&gt; file:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
 * Callback to map the block name to a template.
 * @param $block
 * @return void
 */
function my_acf_block_render_callback( $block ) {

    // convert name ("acf/slider") into path friendly slug ("slider")
    $slug = str_replace('acf/', '', $block['name']);

    // include a template part from within the "template-parts/block" folder
    if ( file_exists( get_theme_file_path("/template-parts/block/content-{$slug}.php") ) ) {
        include( get_theme_file_path("/template-parts/block/content-{$slug}.php") );
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Register the block in your theme’s functions.php file with &lt;a href="https://www.advancedcustomfields.com/resources/acf_register_block_type/" rel="noopener noreferrer"&gt;acf_register_block_type()&lt;/a&gt;.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
 * Example to register acf block.
 */
function my_acf_block_init() {
    // Check if the function exists to avoid errors.
    if (function_exists('acf_register_block_type')) {
        acf_register_block_type(array(
            'name' =&amp;gt; 'my-nice-block-name',
            'title' =&amp;gt; __('This is my first Block'),
            'description' =&amp;gt; __('This is a custom block created using ACF.'),
            'render_template' =&amp;gt; 'path/to/your/block/template.php',
            'category' =&amp;gt; 'formatting',
            'icon' =&amp;gt; 'admin-comments',
            'keywords' =&amp;gt; array('nice', 'block', 'name'),
        ));
    }
}
add_action('acf/init', 'my_acf_block_init');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;In the WordPress dashboard, navigate to Custom Fields &amp;gt; Add New.&lt;/li&gt;
&lt;li&gt;Create a new field group, and add the fields you need for the block and configure their settings.&lt;/li&gt;
&lt;li&gt;In the field group settings under “Location,” choose “Block” for the rule and set it to “is equal to” your block title (e.g., “This is my first Block”).&lt;/li&gt;
&lt;li&gt;Create a new PHP file in your theme’s &lt;code&gt;template-parts/block&lt;/code&gt; directory to serve as the template file for your custom block. Use the naming convention &lt;code&gt;content-{slug}.php&lt;/code&gt;, where &lt;code&gt;{slug}&lt;/code&gt; is the path-friendly slug derived from the block’s name. For example, if your block’s name is “acf/your-block-name”, create a template file named &lt;code&gt;content-your-block-name.php&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In the newly created template file, write the PHP and HTML code that renders the block. Start with the opening PHP tag, and use ACF’s &lt;code&gt;get_field()&lt;/code&gt; or &lt;code&gt;the_field()&lt;/code&gt; functions to retrieve and display the values of the custom fields you added to the block. These functions allow you to access the data stored in your custom fields and output it in your block’s template.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
 * Example of a block template.
 */
&amp;lt;?php
// Get custom field values
$heading = get_field('heading');
$content = get_field('content');
?&amp;gt;

&amp;lt;!-- Output the HTML structure of the block --&amp;gt;
&amp;lt;div class="my-custom-block"&amp;gt;
    &amp;lt;h2&amp;gt;&amp;lt;?php echo esc_html($heading); ?&amp;gt;&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;&amp;lt;?php echo esc_html($content); ?&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pros of ACF:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;User-friendly interface.&lt;/li&gt;
&lt;li&gt;A wide range of field types to choose from.&lt;/li&gt;
&lt;li&gt;Integrates with other WordPress features like custom post types and taxonomies.&lt;/li&gt;
&lt;li&gt;Extensive documentation and a large user community.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons of ACF:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;It can be challenging for users with limited coding experience.&lt;/li&gt;
&lt;li&gt;Requires a PRO version for some advanced features.&lt;/li&gt;
&lt;li&gt;Performance issues may arise with a high number of custom fields.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2.- LazyBlocks
&lt;/h2&gt;

&lt;p&gt;LazyBlocks is a Gutenberg blocks visual constructor that allows you to create custom Gutenberg blocks using a simple drag-and-drop interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating Blocks with LazyBlocks:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Install and activate the &lt;a href="https://wordpress.org/plugins/lazy-blocks/" rel="noopener noreferrer"&gt;LazyBlocks plugin&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;In the WordPress dashboard, navigate to LazyBlocks &amp;gt; Add New. &lt;/li&gt;
&lt;li&gt;Enter the block name and slug, then start adding controls (fields) using the drag-and-drop interface. &lt;/li&gt;
&lt;li&gt;Configure the block output by choosing between PHP, HTML, or Handlebars template.&lt;/li&gt;
&lt;li&gt;Add the block output code in the provided editor or create a separate template file in your theme. Below are the advantages for each option:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PHP:&lt;/strong&gt; Provides full flexibility and control over your block’s output, and allows you to use any PHP functionality to customize your block’s appearance and behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTML:&lt;/strong&gt; Offers a simple and straightforward way to define your block’s output without dealing with PHP code. Ideal for users who prefer working with HTML and CSS only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handlebars template:&lt;/strong&gt; Combines the simplicity of HTML output with the power of conditional logic and loops, making it a versatile option for users who need more control over their block’s output without writing PHP.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save the block, and it will be available in the Gutenberg editor.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Pros of LazyBlocks:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Intuitive drag-and-drop interface.&lt;/li&gt;
&lt;li&gt;No coding required for basic blocks.&lt;/li&gt;
&lt;li&gt;Works with Custom Post Types and Custom Taxonomies.&lt;/li&gt;
&lt;li&gt;A decent number of field types available.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons of LazyBlocks:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Less comprehensive documentation compared to ACF.&lt;/li&gt;
&lt;li&gt;Limited field types compared to ACF.&lt;/li&gt;
&lt;li&gt;Fewer features and options compared to ACF.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wordpress</category>
      <category>gutenberg</category>
    </item>
    <item>
      <title>Demystifying OAuth: A Comprehensive Guide to Understanding and Implementing Open Authorization</title>
      <dc:creator>David Loor</dc:creator>
      <pubDate>Sun, 19 Mar 2023 17:54:35 +0000</pubDate>
      <link>https://dev.to/davo20019/demystifying-oauth-a-comprehensive-guide-to-understanding-and-implementing-open-authorization-3kkb</link>
      <guid>https://dev.to/davo20019/demystifying-oauth-a-comprehensive-guide-to-understanding-and-implementing-open-authorization-3kkb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F03%2FOauth_logo.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2023%2F03%2FOauth_logo.svg" alt="OAuth logo" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In today’s interconnected world, users often need to give applications access to their data stored on other platforms. OAuth (Open Authorization) is an open standard that provides a secure way for users to grant third-party applications access to their resources without sharing their credentials. In this blog post, we’ll dive deep into what OAuth is, how it works, and how to implement it in your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is OAuth?
&lt;/h2&gt;

&lt;p&gt;OAuth is a token-based authentication and authorization protocol that allows third-party applications to access users’ resources on a service without sharing their credentials (e.g., username and password). OAuth is widely used to enable secure communication between different web applications and services, such as when a user logs in to a third-party app using their Google or Facebook account.&lt;/p&gt;

&lt;h2&gt;
  
  
  How OAuth Works
&lt;/h2&gt;

&lt;p&gt;OAuth involves a multi-step process that includes the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Registration&lt;/strong&gt; : The third-party application (client) must first register with the service provider (e.g., Google, Facebook) to obtain a client ID and a client secret. The client ID and secret are unique identifiers that the service provider uses to recognize the application during the OAuth process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization Request&lt;/strong&gt; : When a user wants to use the third-party application, the application directs the user to the service provider’s authorization server. The user is prompted to log in (if not already logged in) and grant the application permission to access their data. The authorization request typically includes the client ID, requested scopes (permissions), and a redirect URL where the user will be sent after granting or denying permission.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization Grant&lt;/strong&gt; : If the user grants permission, the service provider’s authorization server sends an authorization grant (usually a code) back to the third-party application. This is typically done via a redirect URL, which includes the grant as a URL parameter. The type of authorization grant depends on the OAuth grant type being used (e.g., authorization code, implicit).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access Token Request&lt;/strong&gt; : The third-party application sends the authorization grant (code) to the service provider’s token endpoint, along with the client ID and client secret. This step is done server-to-server to ensure the client secret remains confidential. The application also sends the redirect URL for validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access Token Response&lt;/strong&gt; : If the authorization grant is valid, the service provider’s token endpoint returns an access token and, optionally, a refresh token. The access token is a string that represents the user’s authorization to access their resources, while the refresh token can be used to obtain new access tokens when the current one expires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessing Protected Resources&lt;/strong&gt; : The third-party application can now use the access token to make API requests to the service provider on behalf of the user, without the need to access their credentials. The access token is typically included in the HTTP header of the API request, as a Bearer token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refreshing Tokens&lt;/strong&gt; : If the access token has an expiration time, the third-party application may need to use the refresh token to obtain a new access token when the old one expires. The application sends the refresh token to the service provider’s token endpoint, along with the client ID and client secret, to request a new access token. The service provider then returns a new access token and, optionally, a new refresh token.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementing OAuth in Your Application
&lt;/h2&gt;

&lt;p&gt;To implement OAuth in your application, follow these general steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose an OAuth provider (e.g., Google, Facebook) and register your application to obtain a client ID and client secret.&lt;/li&gt;
&lt;li&gt;Set up a callback/redirect URL on your application to handle authorization responses from the OAuth provider.&lt;/li&gt;
&lt;li&gt;Implement the OAuth flow using an appropriate grant type, following the provider’s documentation.&lt;/li&gt;
&lt;li&gt;Implement the necessary code to use the access token to make API requests to the OAuth provider on behalf of the user.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;OAuth provides a secure and user-friendly way to allow third-party applications to access users’ resources on other platforms. Understanding how OAuth works and how to implement it in your application can greatly enhance your app’s security and user experience.&lt;/p&gt;

</description>
      <category>oauth</category>
    </item>
    <item>
      <title>Setting Up a Drupal 9 Multisite Locally with DDEV: Hosted on Acquia with Acquia Pipelines</title>
      <dc:creator>David Loor</dc:creator>
      <pubDate>Mon, 13 Mar 2023 00:40:00 +0000</pubDate>
      <link>https://dev.to/davo20019/setting-up-a-drupal-9-multisite-locally-with-ddev-hosted-on-acquia-with-acquia-pipelines-53ag</link>
      <guid>https://dev.to/davo20019/setting-up-a-drupal-9-multisite-locally-with-ddev-hosted-on-acquia-with-acquia-pipelines-53ag</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2022%2F12%2Fcu2nf0jz5pi.jpg%3Fresize%3D800%252C539%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Fdavidloor.com%2Fwp-content%2Fuploads%2F2022%2F12%2Fcu2nf0jz5pi.jpg%3Fresize%3D800%252C539%26ssl%3D1" alt="Drupal 10" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my current job, the team that I lead is providing support to three websites of a large private organization. The three websites are hosted on Acquia. The websites are part of a multisite setup and the source code is managed using the &lt;a href="https://docs.acquia.com/pipelines/" rel="noopener noreferrer"&gt;Acquia Pipelines&lt;/a&gt; service.&lt;/p&gt;

&lt;p&gt;In a nutshell, &lt;a href="https://docs.acquia.com/pipelines/" rel="noopener noreferrer"&gt;Acquia Pipelines&lt;/a&gt; is a continuous integration and deployment service provided by Acquia, a cloud-based platform that provides enterprise-level hosting, support, and management for Drupal websites.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is DDEV?
&lt;/h2&gt;

&lt;p&gt;DDEV is a command-line tool that helps developers set up local development environments quickly and easily. It is designed to simplify the process of setting up a local web server, managing dependencies, and configuring development environments. If this is the first time that you are trying to work with DDEV, please make sure that you check this &lt;a href="https://dev.to/davo20019/how-to-setup-a-wordpress-or-drupal-site-locally-with-ddev-4035-temp-slug-181423"&gt;post&lt;/a&gt; to get started with DDEV.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a Drupal multisite?
&lt;/h2&gt;

&lt;p&gt;Drupal multisite is a feature that allows multiple websites to be managed using a single Drupal codebase and database. In a Drupal multisite setup, multiple websites share a single Drupal installation, which can reduce maintenance time and costs.&lt;/p&gt;

&lt;p&gt;Each website in a Drupal multisite configuration has its own unique domain or subdomain and can have its own theme, modules, content, and configurations. However, they share the same core codebase and database.&lt;/p&gt;

&lt;p&gt;Drupal multisite is useful when you need to manage multiple websites that share a similar codebase, theme, or functionality, such as a network of related websites, or multiple language versions of a website. It can simplify website maintenance, reduce costs, and improve consistency across websites.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recipe to setup the local multisite
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;You need to set up the project using DDEV. Check this &lt;a href="https://dev.to/davo20019/how-to-setup-a-wordpress-or-drupal-site-locally-with-ddev-4035-temp-slug-181423"&gt;post&lt;/a&gt; out to get started.

&lt;ul&gt;
&lt;li&gt;The docroot location from where the site is served should be &lt;strong&gt;web/docroot.&lt;/strong&gt; You will be asked a question about the docroot location when running the command &lt;code&gt;ddev config&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In the .ddev folder, create a file name &lt;strong&gt;config.multisite.yaml&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The file &lt;strong&gt;config.multisite.yaml&lt;/strong&gt; should have the following content:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;additional_hostnames:
  - site1
  - site2
  - site3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Update the sites.php file to add the three local domains and their corresponding folders.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The sites.php file is a configuration file that allows you to map domain names or URLs to specific Drupal site directories.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$sites['site1.ddev.site'] = 'site1_directory';
$sites['site2.ddev.site'] = 'site2_directory';
$sites['site3.ddev.site'] = 'site3_directory';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Create three databases using &lt;a href="https://www.phpmyadmin.net/" rel="noopener noreferrer"&gt;PHPMyAdmin&lt;/a&gt;. If the name of the DDEV project were &lt;strong&gt;mysite&lt;/strong&gt; , you can access phpmyadmin at &lt;a href="https://mysite.ddev.site:8037/" rel="noopener noreferrer"&gt;https://mysite.ddev.site:8037/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Export database backups from acquia and import them into the corresponding databases that you created as part of step 4.

&lt;ul&gt;
&lt;li&gt;If the databases are big, you can use a ddev command to import them: &lt;code&gt;ddev import-db --target-db=DATABASE_NAME --src=.tarballs/DB-BACKUP_FILE.sql.gz&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In the directory of each file, you need to add this ddev &lt;a href="https://github.com/davo20019/ddev-local-multisite/blob/main/settings.ddev.php" rel="noopener noreferrer"&gt;file&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The settings.php file of each site needs to be updated to include the code from &lt;a href="https://github.com/davo20019/ddev-local-multisite/blob/main/settings.php" rel="noopener noreferrer"&gt;this file&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;ddev ssh&lt;/code&gt; and after that &lt;code&gt;composer install&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;code&gt;ddev ssh&lt;/code&gt; command allows you to access the command line interface (CLI) of a running ddev web container, so you can run composer, drush and other commands from inside the web container.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>drupal</category>
      <category>acquia</category>
    </item>
  </channel>
</rss>
