Setting up Matomo w/ Wordpress + React

Absolutely—here’s the cheat-sheet with the “why” behind every call, so Future-You knows exactly what each one proves.

0) Quick map of the stack

  • NGINX (80/443, public) → proxies to Apache (8080, local) for app sites.
  • Tracker is served directly by NGINX (its own vhost + PHP-FPM). tracker.ferociousbutterfly.com
  • Let’s Encrypt uses HTTP-01 files under /.well-known/acme-challenge/ served by NGINX from /var/www/letsencrypt.

1) Port & service reality checks (what’s actually listening)

sudo ss -tulpn | grep ':80\|:443\|:8080'

Why: Confirms which process owns which port (e.g., NGINX on 80/443, Apache on 8080). If 8080 is absent, NGINX will 502 because there’s no upstream.

sudo systemctl status apache2

Why: Tells you if Apache is running. If “inactive (dead)”, NGINX can’t proxy to it.

2) Apache correctness (is it bound to 8080 and serving the right vhosts?)

grep -n "Listen " /etc/apache2/ports.conf

Why: Verifies Apache is configured to Listen 8080 (and not 80). Avoids port conflicts with NGINX.

sudo apachectl -S

Why: Shows Apache’s active vhosts and which ServerName maps to which vhost. Ensures your flap-flap and wp-flap-flap *:8080 vhosts are enabled and will be selected correctly when NGINX forwards the Host header.

curl -I -H 'Host: flap-flap.ferociousbutterfly.com' http://127.0.0.1:8080/
curl -I -H 'Host: wp-flap-flap.ferociousbutterfly.com' http://127.0.0.1:8080/

Why: Hits Apache directly (bypassing NGINX) to prove the upstream is alive and vhost routing works by Host header.

3) NGINX vhost routing (who answers which hostname?)

sudo nginx -T | grep -nA3 -B2 'server_name'

Why: Dumps the effective NGINX config, so you can see exactly which server_name blocks are loaded (no guessing about conf.d vs sites-enabled).

curl -i -H 'Host: ferociousbutterfly.com'     http://127.0.0.1/
curl -i -H 'Host: www.ferociousbutterfly.com' http://127.0.0.1/
curl -ik -H 'Host: ferociousbutterfly.com'    https://127.0.0.1/
curl -ik -H 'Host: www.ferociousbutterfly.com'https://127.0.0.1/

Why: Simulates requests as if they came from the internet but locally. Tells you which NGINX server{} actually answers HTTP and HTTPS for apex/www. If HTTPS returns odd content, you’re missing a matching listen 443 ssl; server_name … block.

4) ACME/Let’s Encrypt path (will HTTP-01 pass?)

sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge
sudo chown -R www-data:www-data /var/www/letsencrypt

Why: Ensures the webroot exists and is readable by NGINX.

# Sanity “ping” file
echo ok | sudo tee /var/www/letsencrypt/.well-known/acme-challenge/ping >/dev/null
curl -I http://ferociousbutterfly.com/.well-known/acme-challenge/ping
curl -I http://www.ferociousbutterfly.com/.well-known/acme-challenge/ping
curl -I http://flap-flap.ferociousbutterfly.com/.well-known/acme-challenge/ping
curl -I http://wp-flap-flap.ferociousbutterfly.com/.well-known/acme-challenge/ping

Why: Must return 200 OK over HTTP. If you see 301 to HTTPS, your redirect is catching the ACME path; fix by moving the redirect into location / { return 301 … } and keeping a specific location ^~ /.well-known/acme-challenge/ { … } above it.

5) Issue/renew certificates (the minimum that works)

sudo certbot certificates

Why: Inventory of certs, expiry, and key type (RSA/ECDSA). Helps decide what to renew.

# Using webroot so NGINX can keep running
sudo certbot certonly --webroot -w /var/www/letsencrypt -d ferociousbutterfly.com -d www.ferociousbutterfly.com  --key-type rsa
sudo certbot certonly --webroot -w /var/www/letsencrypt -d flap-flap.ferociousbutterfly.com
sudo certbot certonly --webroot -w /var/www/letsencrypt -d wp-flap-flap.ferociousbutterfly.com

Why: Issues/renews certs without stopping NGINX (no standalone binding to :80). Add all names you want on the apex cert (-d ferocious… -d http://www.…).

systemctl list-timers | grep certbot

Why: Confirms the auto-renew systemd timer is active. It checks twice daily and renews when ≤30 days remain.

6) Make NGINX actually use the fresh certs

sudo nginx -t && sudo systemctl reload nginx

Why: Reload is enough to pick up new fullchain.pem/privkey.pem without dropping connections.

# Prove what’s on the wire (not browser cache)
echo | openssl s_client -servername ferociousbutterfly.com        -connect ferociousbutterfly.com:443        2>/dev/null | openssl x509 -noout -dates -subject
echo | openssl s_client -servername www.ferociousbutterfly.com    -connect www.ferociousbutterfly.com:443    2>/dev/null | openssl x509 -noout -dates -subject
echo | openssl s_client -servername flap-flap.ferociousbutterfly.com -connect flap-flap.ferociousbutterfly.com:443 2>/dev/null | openssl x509 -noout -dates -subject
echo | openssl s_client -servername wp-flap-flap.ferociousbutterfly.com -connect wp-flap-flap.ferociousbutterfly.com:443 2>/dev/null | openssl x509 -noout -dates -subject

Why: Shows the actual cert being served (subject + expiry). If the dates aren’t new, your ssl_certificate paths or SNI server_name don’t match.

7) Mixed-content check (why browser still says Not Secure)

curl -s https://ferociousbutterfly.com/ | grep -i 'http://' | head
curl -s https://wp-flap-flap.ferociousbutterfly.com/ | grep -i 'http://' | head

Why: Finds hard-coded http:// assets that cause “Not secure” even with a valid cert. For WordPress, set WP_HOME/WP_SITEURL to https://… and force HTTPS behind the proxy.

8) Tracker (Matomo) perms (why updates failed)

sudo chown -R www-data:www-data /var/www/html/tracker
sudo find /var/www/html/tracker -type f -exec chmod 644 {} \;
sudo find /var/www/html/tracker -type d -exec chmod 755 {} \;
sudo chmod 755 /var/www/html/tracker/console

Why: Matomo needs writable dirs and executable console; wrong perms cause update/plug-in errors.

9) “One-shot health check” (what it proves)

for d in ferociousbutterfly.com www.ferociousbutterfly.com flap-flap.ferociousbutterfly.com wp-flap-flap.ferociousbutterfly.com tracker.ferociousbutterfly.com; do
  echo -e "\n🔍 $d"
  echo | openssl s_client -servername $d -connect $d:443 2>/dev/null | openssl x509 -noout -dates -issuer -subject
done

Why: Confirms each hostname serves a valid, unexpired cert (and which CA issued it). If any fail, you know to check the matching NGINX server{} and the ACME path for that name.

10) Two pitfalls to avoid (the ones that bit us)

  • Server-level return 301 on the HTTP block: it runs before location matching and hijacks /.well-known/acme-challenge/. Fix by putting the redirect inside location / { … } and adding a specific ACME location above it.
  • Missing 443 block for a name: HTTP works but HTTPS falls back to some other vhost. Add a matching server_name …; listen 443 ssl; with the right cert paths.

If you want, I can package this as /root/ferociousbutterfly-ssl-nginx-cheatsheet.md on the box so it’s always right there next time.


URL SPECIFIC INFO

>

Ferocious Butterfly

The Setup

To ensure your server is properly configured for running both Laravel and a headless WordPress setup, there are a few key configurations you'll need to check and set up:

1. General Server Requirements

Both Laravel and WordPress have specific system requirements, and you'll want to make sure your server is prepared to handle them. Here's a breakdown:

Operating System

  • Ensure you're using a Linux-based server like Ubuntu or AlmaLinux (which you’re already using). Make sure the server is up to date:

    sudo yum update  # For AlmaLinux
    

PHP Requirements

Both Laravel and WordPress require PHP, but Laravel (latest versions) often requires newer PHP versions than WordPress.

  • Laravel (PHP 8.x required):

    sudo yum install php php-mbstring php-xml php-bcmath php-json php-zip
    
  • WordPress (PHP 7.4+ minimum):
    You'll want to install or ensure the following PHP extensions are available for WordPress:

    sudo yum install php php-mysqlnd php-gd php-xml php-json php-curl php-xmlrpc php-imagick php-intl
    

Composer for Laravel

Laravel requires Composer to manage dependencies. Ensure Composer is installed:

curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

Node.js and NPM (for Laravel with React)

Laravel Mix (used to compile assets) requires Node.js and npm. You likely already have this set up, but verify:

sudo yum install nodejs npm

2. Web Server (Apache) Configuration

You’re using Apache, so you'll want to configure it to handle both WordPress and Laravel:

Virtual Hosts Setup

You'll need to set up virtual hosts for each application (Laravel and WordPress) to serve them under different domains or subdomains.

Example configuration for Apache:

  1. Laravel Virtual Host:

    <VirtualHost *:80>
        ServerName flap-flap.ferociousbutterfly.com
        DocumentRoot /var/www/flap-flap.ferociousbutterfly.com/public
        
        <Directory /var/www/flap-flap.ferociousbutterfly.com/public>
            AllowOverride All
            Require all granted
        </Directory>
    
        ErrorLog ${APACHE_LOG_DIR}/laravel_error.log
        CustomLog ${APACHE_LOG_DIR}/laravel_access.log combined
    </VirtualHost>
    
  2. WordPress Virtual Host:

    <VirtualHost *:80>
        ServerName wp.flap-flap.ferociousbutterfly.com
        DocumentRoot /var/www/wp-flap-flap.ferociousbutterfly.com
    
        <Directory /var/www/wp-flap-flap.ferociousbutterfly.com>
            AllowOverride All
            Require all granted
        </Directory>
    
        ErrorLog ${APACHE_LOG_DIR}/wordpress_error.log
        CustomLog ${APACHE_LOG_DIR}/wordpress_access.log combined
    </VirtualHost>
    

Mod_Rewrite for Apache

Both Laravel and WordPress require mod_rewrite to handle pretty URLs. Make sure it’s enabled:

sudo a2enmod rewrite
sudo systemctl restart httpd

3. Database (MySQL or MariaDB)

Both Laravel and WordPress use databases (typically MySQL or MariaDB). You’ll need to ensure MariaDB is installed and configured correctly. You can host both applications on the same database server, but it’s better to create separate databases.

  1. Install MariaDB:

    sudo yum install mariadb-server mariadb
    sudo systemctl start mariadb
    sudo systemctl enable mariadb
    
  2. Set up the databases:

    CREATE DATABASE laravel_db;
    CREATE DATABASE wordpress_db;
    
    CREATE USER 'laravel_user'@'localhost' IDENTIFIED BY 'password';
    CREATE USER 'wordpress_user'@'localhost' IDENTIFIED BY 'password';
    
    GRANT ALL PRIVILEGES ON laravel_db.* TO 'laravel_user'@'localhost';
    GRANT ALL PRIVILEGES ON wordpress_db.* TO 'wordpress_user'@'localhost';
    
    FLUSH PRIVILEGES;
    

4. SSL Configuration

If you plan to use HTTPS, you'll need SSL certificates. You can use Let's Encrypt for free SSL certificates. Install Certbot and configure SSL for both Laravel and WordPress.

  1. Install Certbot:

    sudo yum install certbot python3-certbot-apache
    
  2. Get SSL Certificates:

    sudo certbot --apache -d flap-flap.ferociousbutterfly.com -d wp.flap-flap.ferociousbutterfly.com
    
  3. Auto-Renew SSL:
    Certbot automatically sets up renewal, but you can manually test it:

    sudo certbot renew --dry-run
    

The steps you're planning, including enabling mod_rewrite and setting up WordPress, should not affect the functioning of Matomo as long as they are set up independently in separate virtual hosts with their own directories. Your current UFW firewall rules look fine as they allow traffic on ports 80 and 443, which is necessary for both Matomo and your WordPress/Laravel setup.

A few points to ensure smooth operation:

  1. Virtual Hosts Separation: Each domain/subdomain (flap-flap.ferociousbutterfly.com, tracker.ferociousbutterfly.com, etc.) is configured as its own Apache virtual host, which means they won't interfere with each other unless there’s a misconfiguration in the Apache settings. Since Matomo is served through tracker.ferociousbutterfly.com, it operates independently from the wp-flap-flap.ferociousbutterfly.com domain, so installing WordPress won’t impact Matomo.
  2. Matomo Database: Since you have separate databases for Matomo, WordPress, and Laravel, the database activities related to WordPress or Laravel won’t interfere with Matomo's data collection or reporting functions.
  3. mod_rewrite: Enabling mod_rewrite is common and required for WordPress and Laravel. Matomo itself doesn’t heavily depend on Apache's mod_rewrite unless you've configured specific redirects within your Matomo installation, which is uncommon. This step should be safe.
  4. SSL Redirection: You’ve already included SSL redirection in both virtual host configurations, which looks fine. Just ensure that your SSL certificates (for all domains) are properly configured.
  5. Running Commands as Root: You’re receiving a warning when running Composer as root. It’s recommended to run Composer commands under a different user (not root) for security reasons. You can create a non-root user and give them permission to manage your Laravel and WordPress installations.

Here’s how to create a non-root user if needed:

sudo adduser yourusername
sudo usermod -aG sudo yourusername

Then switch to that user before running Composer or other commands:

su - yourusername

Once you’ve confirmed the separation of your domains and configurations, you should be safe to proceed with:

sudo a2enmod rewrite
sudo systemctl restart apache2

5. WordPress Configuration (Headless)

  1. Install WordPress:
    Download and install WordPress in the /var/www/wp-flap-flap.ferociousbutterfly.com directory:

    wget https://wordpress.org/latest.tar.gz
    tar -xvzf latest.tar.gz
    mv wordpress /var/www/wp-flap-flap.ferociousbutterfly.com/public/
    
  2. Configure WordPress:

    • Update the wp-config.php file with your database credentials and set the appropriate SITEURL and HOME for the headless setup.
  3. Enable REST API:
    WordPress’s REST API is enabled by default. You can access it through:

    http://wp.flap-flap.ferociousbutterfly.com/wp-json/wp/v2/
    

6. Security

  • Firewalls: Ensure ports 80 (HTTP) and 443 (HTTPS) are open.

    sudo firewall-cmd --permanent --add-service=http
    sudo firewall-cmd --permanent --add-service=https
    sudo firewall-cmd --reload
    
  • File Permissions: Ensure proper file permissions for both Laravel and WordPress to avoid issues with file uploads and security.

    sudo chown -R www-data:www-data /var/www/flap-flap.ferociousbutterfly.com
    sudo chown -R www-data:www-data /var/www/wp-flap-flap.ferociousbutterfly.com
    

7. Caching and Performance (Optional)

  • OPcache: Enable PHP OPcache for performance.

    sudo yum install php-opcache
    
  • Caching Plugins: Use a caching plugin for WordPress like W3 Total Cache.

With these configurations, your server should be ready to run both Laravel and a headless WordPress setup. Would you like to proceed with setting up WordPress, or do you need help with any of these configurations?

setting up wp / frontenbd

Great! Let’s set up your WordPress installation as a headless CMS to serve content via the REST API, and then we’ll move on to connecting it with your React front end.

Step 1: Ensure REST API Access is Enabled

WordPress has the REST API enabled by default. You can test it by visiting the following URL in your browser:

https://wp-flap-flap.ferociousbutterfly.com/wp-json/wp/v2/posts

This should return a JSON response containing any posts in WordPress (if you haven’t created any posts yet, it will return an empty array).

Step 2: Create Sample Content in WordPress

Before connecting WordPress to the React front end, you’ll want to create some sample content that can be fetched via the REST API.

  1. Create Posts:

    • Go to https://wp-flap-flap.ferociousbutterfly.com/wp-admin/.
    • Create a couple of test posts under Posts > Add New.
    • Publish these posts, so you can retrieve them through the API.
  2. Optional: Install Useful Plugins
    You might want to install some plugins to enhance the REST API or your headless setup:

    • Headless WordPress Plugin: Disable WordPress's front-end rendering and only serve content via API.
    • Custom Post Types: If you want to serve more complex content, you can create custom post types with plugins like Custom Post Type UI.

Step 3: Configure WordPress Settings for Headless Setup

To prevent users from accessing the default WordPress front end, you can install a plugin like "Disable Frontend," or use a simple redirect in your theme’s functions.php:

  1. Redirect WordPress Frontend:
    Add this code to your theme’s functions.php file (or create a custom plugin if preferred) to redirect all non-logged-in users to your React app:

    function redirect_to_frontend() {
        if (!is_admin() && !is_user_logged_in()) {
            wp_redirect('https://flap-flap.ferociousbutterfly.com');
            exit();
        }
    }
    add_action('template_redirect', 'redirect_to_frontend');
    

This ensures the WordPress admin is still accessible but the default front-end will be redirected.

Step 4: Setting Up the React Front End

Now, we can move on to your React front end, which will fetch content from WordPress.

  1. Set Up Axios or Fetch API:
    In your React project, you’ll need a way to make HTTP requests. You can use either Axios or the native Fetch API to interact with the WordPress REST API.

    Install Axios if you want to use it:

    npm install axios
    
  2. Create a Service to Fetch Posts from WordPress:
    Create a service or hook in React to fetch posts from the WordPress REST API.

    Here’s an example using Axios:

    import axios from 'axios';
    import { useEffect, useState } from 'react';
    
    const useFetchPosts = () => {
      const [posts, setPosts] = useState([]);
      const [loading, setLoading] = useState(true);
    
      useEffect(() => {
        axios.get('https://wp-flap-flap.ferociousbutterfly.com/wp-json/wp/v2/posts')
          .then(response => {
            setPosts(response.data);
            setLoading(false);
          })
          .catch(error => {
            console.error("There was an error fetching the posts!", error);
            setLoading(false);
          });
      }, []);
    
      return { posts, loading };
    };
    
    export default useFetchPosts;
    
  3. Display Posts in a React Component:
    Now, use the useFetchPosts hook in your component to display the posts:

    import React from 'react';
    import useFetchPosts from './useFetchPosts';  // Adjust path accordingly
    
    const PostsList = () => {
      const { posts, loading } = useFetchPosts();
    
      if (loading) return <div>Loading...</div>;
    
      return (
        <div>
          {posts.map(post => (
            <div key={post.id}>
              <h2>{post.title.rendered}</h2>
              <div dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
            </div>
          ))}
        </div>
      );
    };
    
    export default PostsList;
    
  4. Add PostsList to Your App:
    Finally, add the PostsList component to your main React app (e.g., in App.js).

    import React from 'react';
    import PostsList from './components/PostsList';  // Adjust path accordingly
    
    const App = () => {
      return (
        <div className="App">
          <PostsList />
        </div>
      );
    };
    
    export default App;
    

Step 5: Test the Front End

Once you've connected the React app to WordPress via the REST API, deploy the front-end changes and test fetching the posts on flap-flap.ferociousbutterfly.com.

scp -r /Users/spiffy/Documents/nsgandolf/dit-dah/wp-flap-flap/build/* root@45.132.241.65:/var/www/flap-flap.ferociousbutterfly.com/public/

laravel config

<VirtualHost *:80>
    ServerName flap-flap.ferociousbutterfly.com
    DocumentRoot /var/www/flap-flap.ferociousbutterfly.com/public

    <Directory /var/www/flap-flap.ferociousbutterfly.com/public>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/flap-flap.ferociousbutterfly.com_error.log
    CustomLog ${APACHE_LOG_DIR}/flap-flap.ferociousbutterfly.com_access.log combined

    # Redirect all HTTP requests to HTTPS
    RewriteEngine on
    RewriteCond %{SERVER_NAME} =flap-flap.ferociousbutterfly.com
    RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>



React Config

<VirtualHost *:80>
    ServerName flap-flap.ferociousbutterfly.com
    DocumentRoot /var/www/flap-flap.ferociousbutterfly.com/public

    <Directory /var/www/flap-flap.ferociousbutterfly.com/public>
        AllowOverride All
        Require all granted
        Options -Indexes
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/flap-flap_error.log
    CustomLog ${APACHE_LOG_DIR}/flap-flap_access.log combined
</VirtualHost>

reconfigure and optimize

sudo a2ensite flap-flap.ferociousbutterfly.com.conf
sudo systemctl restart apache2

Step 6: (Optional) Caching and Optimization

To optimize performance:

  • Caching: Use caching strategies in React (e.g., React Query, localStorage) to reduce the number of API calls.
  • Pagination: Implement pagination if your WordPress site will have many posts.

Let me know if you'd like to dive deeper into any of these steps or need additional help with configurations!

Yes, you can absolutely use Advanced Custom Fields (ACF) to add custom class names to almost any element in WordPress. This approach will give you complete control over which elements receive specific class names, without relying on captions or other user-facing content.

Here’s how you can set this up:

Step 1: Create a Custom Field in ACF

  1. Install and Activate ACF if you haven’t done so already.

  2. Create a New Field Group:

    • Go to Custom Fields > Add New.
    • Name the field group something like "Element Class Names."
  3. Add a New Field:

    • Add a field called something like "Custom Class Name."
    • Set the Field Type to Text.
    • Optionally, you can add a default value or placeholder text, like "Enter class name here."
  4. Set the Location Rules:

    • Choose where this field should appear. For example, you can assign this field to posts, pages, images, or custom post types.
  5. Publish the Field Group.

Step 2: Update Your Template to Use the Custom Class Name

Now that the custom field is set up, you’ll need to modify your WordPress theme template files to use the custom class name where needed.

For example, if you’re adding a custom class to an image or a post, you can modify the relevant template like this:

Example for Posts:

In your theme’s template file (e.g., single.php, content.php, or any custom template), modify the HTML where you want the class to be applied:

<?php
// Get the custom class name from ACF
$custom_class = get_field('custom_class_name');

// If a custom class is set, apply it to the post container
?>
<div class="post-container <?php echo esc_attr($custom_class); ?>">
    <h2><?php the_title(); ?></h2>
    <div class="post-content">
        <?php the_content(); ?>
    </div>
</div>

Example for Images:

If you want to use the custom class for an image, you can modify the template where the image is displayed:

<?php
// Get the custom class name from ACF for an image
$custom_class = get_field('custom_class_name');

// Display the image with the custom class if it exists
?>
<img src="<?php echo esc_url($image_url); ?>" class="<?php echo esc_attr($custom_class); ?>" alt="<?php echo esc_attr($alt_text); ?>">

Step 3: Add Class Names in the WordPress Editor

  • When you edit a post, page, or other content type, you will now see the custom "Class Name" field that you added via ACF.
  • You can input any class name you want in this field, and it will be applied to the relevant element in the front end.

Step 4: CSS

Once you have the custom classes added via ACF, you can target them in your CSS for custom styling. For example:

.custom-class-example {
    border: 2px solid blue;
    padding: 10px;
}

.custom-image-class {
    max-width: 100%;
    border-radius: 8px;
}

Benefits of Using ACF for Class Names:

  1. Complete Control: You can specify exactly which elements get custom classes.
  2. No User Confusion: Users won’t have to deal with captions or other fields for styling. Instead, they’ll have a clean input field for class names.
  3. Flexibility: You can use this approach for any type of element—posts, images, custom post types, etc.

Summary:

Using ACF to add custom class names to WordPress elements is a great solution for controlling the design and layout without exposing user-facing content like captions. It gives you flexibility and full control over styling.

Let me know if you'd like to proceed with this setup or need more details!