Building a Blogging Site with React and PHP: A Step-by-Step Guide
Tags: webdev, php, react, bloggingsite
Published: 2024-02-11 13:28:36

Welcome to our comprehensive tutorial on building a React PHP Blogging Site. This step-by-step guide takes you through creating a fully functional blog using the powerful combination of [React](https://www.webdevstory.com/mastering-es6-for-react/) for the front end and PHP for the back end.

* CRUD Operations
    
* Like/Dislike Feature
    

By the end of this tutorial, I hope you will have a clear understanding of how to integrate a React frontend with a PHP backend, along with a functional blogging site you can continue to expand and customize.

Let’s start this exciting project and bring our blogging site to life!

## Essential Tools for Our React PHP Blogging Site Tutorial

* React
    
* PHP
    
* MySQL
    
* Axios
    
* Bootstrap
    

## **Environment Variables**

To handle our API endpoint configurations, we use an `.env` file in our React PHP Blogging Platform.

```javascript
REACT_APP_API_BASE_URL=http://localhost/Projects/blogging-stie/server/api
```

## Database Schema

Our blogging site has primarily two tables to store data: `blog_posts` for the blog entries and `post_votes` for counting likes and dislikes.

```javascript
CREATE TABLE `blog_posts`
(
    `id`           INT(11) NOT NULL AUTO_INCREMENT,
    `title`        VARCHAR(255) NOT NULL,
    `author`       VARCHAR(255) NOT NULL,
    `content`      TEXT NOT NULL,
    `publish_date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `post_votes`
(
    `id`         INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `post_id`    INT(11) NOT NULL,
    `user_ip`    VARCHAR(50) NOT NULL,
    `vote_type`  ENUM('like', 'dislike') NOT NULL,
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (`post_id`) REFERENCES `blog_posts` (`id`)
        ON DELETE CASCADE
        ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

## Configuring CORS

In today’s web application, security is crucial. To enable safe cross-origin requests, we implement CORS policies in our `config.php` file.

## Key Components of Our CORS Configuration

* Allowed Origins
    
* Allowed Headers
    
* Handling Preflight Requests
    

```javascript
// Define configuration options
$allowedOrigins = ['http://localhost:3000'];
$allowedHeaders = ['Content-Type'];

// Set headers for CORS
$origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '';
if (in_array($origin, $allowedOrigins)) {
    header('Access-Control-Allow-Origin: ' . $origin);
}

if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) {
    header('Access-Control-Allow-Methods: ' . implode(', ', $allowedMethods));
}

if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
    $requestHeaders = explode(',', $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']);
    $requestHeaders = array_map('trim', $requestHeaders); // Trim whitespace from headers
    if (count(array_intersect($requestHeaders, $allowedHeaders)) == count($requestHeaders)) {
        header('Access-Control-Allow-Headers: ' . implode(', ', $allowedHeaders));
    }
}
```

## Database Configuration and Connection

To store and manage the data for our blogging platform, we use a `MySQL` database.

```javascript
<?php
// Database configuration
$dbHost     = "";
$dbUsername = "";
$dbPassword = "";
$dbName     = "";

// Create database connection
$conn = new mysqli($dbHost, $dbUsername, $dbPassword, $dbName);

// Check connection
if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
}
```

## Create Single Post

### Core Features of the CreatePost Component

* State Management with Hooks
    
* Form Validation
    
* Asynchronous Data Handling
    
* Navigation and Feedback
    

```javascript
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';

function CreatePost() {
    const [title, setTitle] = useState('');
    const [content, setContent] = useState('');
    const [author, setAuthor] = useState('');
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState(''); // State for storing the error message

    const navigate = useNavigate();

    // Example validation function (extend as needed)
    const validateForm = () => {
        if (!title.trim() || !content.trim() || !author.trim()) {
            setError("Please fill in all fields.");
            return false;
        }
        // Additional validation logic here
        return true;
    };

    const handleSubmit = async (event) => {
        event.preventDefault();
        setError(''); // Reset error message on new submission
        if (!validateForm()) return; // Perform validation

        setIsLoading(true);

        try {
            const response = await axios.post(`${process.env.REACT_APP_API_BASE_URL}/create-post.php`, {
                title,
                content,
                author
            });
            console.log(response.data);
            navigate('/');
        } catch (error) {
            console.error(error);
            setError('Failed to create post. Please try again later.');
            setIsLoading(false);
        }
    };

    return (
        <div className="container mt-4">
            <h2>Create a New Post</h2>
            {error && <div className="alert alert-danger" role="alert">{error}</div>} {/* Display error message */}
            <form onSubmit={handleSubmit}>
                <div className="mb-3">
                    <label htmlFor="title" className="form-label">Title</label>
                    <input
                        type="text"
                        className="form-control"
                        id="title"
                        value={title}
                        onChange={(e) => setTitle(e.target.value)}
                        required
                    />
                </div>
                <div className="mb-3">
                    <label htmlFor="content" className="form-label">Content</label>
                    <textarea
                        className="form-control"
                        id="content"
                        rows="5"
                        value={content}
                        onChange={(e) => setContent(e.target.value)}
                        required
                    ></textarea>
                </div>
                <div className="mb-3">
                    <label htmlFor="author" className="form-label">Author</label>
                    <input
                        type="text"
                        className="form-control"
                        id="author"
                        value={author}
                        onChange={(e) => setAuthor(e.target.value)}
                        required
                    />
                </div>
                <button type="submit" className="btn btn-primary" disabled={isLoading}>
                    {isLoading ? <span><span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Creating post...</span> : 'Create Post'}
                </button>
            </form>
        </div>
    );
}

export default CreatePost;
```

```javascript
header('Access-Control-Allow-Headers: Content-Type'); // Allow Content-Type header

require_once('../config/config.php');
require_once('../config/database.php');

// Retrieve the request body as a string
$request_body = file_get_contents('php://input');

// Decode the JSON data into a PHP array
$data = json_decode($request_body, true);

// Validate input fields with basic validation
if (empty($data['title']) || empty($data['content']) || empty($data['author'])) {
    http_response_code(400);
    echo json_encode(['message' => 'Error: Missing or empty required parameter']);
    exit();
}

// Validate input fields
if (!isset($data['title']) || !isset($data['content']) || !isset($data['author'])) {
    http_response_code(400);
    die(json_encode(['message' => 'Error: Missing required parameter']));
}

// Sanitize input
$title = filter_var($data['title'], FILTER_SANITIZE_STRING);
$author = filter_var($data['author'], FILTER_SANITIZE_STRING);
$content = filter_var($data['content'], FILTER_SANITIZE_STRING);

// Prepare statement
$stmt = $conn->prepare('INSERT INTO blog_posts (title, content, author) VALUES (?, ?, ?)');
$stmt->bind_param('sss', $title, $content, $author);

// Execute statement
if ($stmt->execute()) {
    // Get the ID of the newly created post
    $id = $stmt->insert_id;

    // Return success response
    http_response_code(201);
    echo json_encode(['message' => 'Post created successfully', 'id' => $id]);
} else {
    // Return error response with more detail if possible
    http_response_code(500);
    echo json_encode(['message' => 'Error creating post: ' . $stmt->error]);
}

// Close statement and connection
$stmt->close();
$conn->close();
```

## Display All Posts

```javascript
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';

function PostList() {
    const [posts, setPosts] = useState([]);
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError] = useState('');
    const [currentPage, setCurrentPage] = useState(1);
    const [totalPosts, setTotalPosts] = useState(0);
    const postsPerPage = 10;

    useEffect(() => {
        const fetchPosts = async () => {
            setIsLoading(true);
            try {
                const response = await axios.get(`${process.env.REACT_APP_API_BASE_URL}/posts.php?page=${currentPage}`);
                setPosts(response.data.posts);
                setTotalPosts(response.data.totalPosts);
                setIsLoading(false);
            } catch (error) {
                console.error(error);
                setError('Failed to load posts.');
                setIsLoading(false);
            }
        };

        fetchPosts();
    }, [currentPage]);

    const totalPages = Math.ceil(totalPosts / postsPerPage);
    const goToPreviousPage = () => setCurrentPage(currentPage - 1);
    const goToNextPage = () => setCurrentPage(currentPage + 1);

    return (
        <div className="container mt-5">
            <h2 className="mb-4">All Posts</h2>
            {error && <div className="alert alert-danger">{error}</div>}
            <div className="row">
                {isLoading ? (
                    <p>Loading posts...</p>
                ) : posts.length ? (
                    posts.map(post => (
                        <div className="col-md-6" key={post.id}>
                            <div className="card mb-4">
                                <div className="card-body">
                                    <h5 className="card-title">{post.title}</h5>
                                    <p className="card-text">By {post.author} on {new Date(post.publish_date).toLocaleDateString()}</p>
                                    <Link to={`/post/${post.id}`} className="btn btn-primary">Read More</Link>
                                </div>
                            </div>
                        </div>
                    ))
                ) : (
                    <p>No posts available.</p>
                )}
            </div>
            <nav aria-label="Page navigation">
                <ul className="pagination">
                    <li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
                        <button className="page-link" onClick={goToPreviousPage}>Previous</button>
                    </li>
                    {Array.from({ length: totalPages }, (_, index) => (
                        <li key={index} className={`page-item ${index + 1 === currentPage ? 'active' : ''}`}>
                            <button className="page-link" onClick={() => setCurrentPage(index + 1)}>{index + 1}</button>
                        </li>
                    ))}
                    <li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
                        <button className="page-link" onClick={goToNextPage}>Next</button>
                    </li>
                </ul>
            </nav>
        </div>
    );
}

export default PostList;
```

```javascript
// Load configuration files
require_once('../config/config.php');
require_once('../config/database.php');

header('Content-Type: application/json');

// Define configuration options
$allowedMethods = ['GET'];
$maxPostsPerPage = 10;

// Implement basic pagination
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$offset = ($page - 1) * $maxPostsPerPage;

// Query to count total posts
$countQuery = "SELECT COUNT(*) AS totalPosts FROM blog_posts";
$countResult = mysqli_query($conn, $countQuery);
$countRow = mysqli_fetch_assoc($countResult);
$totalPosts = $countRow['totalPosts'];

// Check if total posts query is successful
if (!$countResult) {
    http_response_code(500); // Internal Server Error
    echo json_encode(['message' => 'Error querying database for total posts count: ' . mysqli_error($conn)]);
    mysqli_close($conn);
    exit();
}

// Query to get all blog posts with pagination and ordering
$query = "SELECT * FROM blog_posts ORDER BY publish_date DESC LIMIT $offset, $maxPostsPerPage";
$result = mysqli_query($conn, $query);

// Check if paginated posts query is successful
if (!$result) {
    http_response_code(500); // Internal Server Error
    echo json_encode(['message' => 'Error querying database for paginated posts: ' . mysqli_error($conn)]);
    mysqli_close($conn);
    exit();
}

// Convert query result into an associative array
$posts = mysqli_fetch_all($result, MYSQLI_ASSOC);

// Check if there are posts
if (empty($posts)) {
    // No posts found, you might want to handle this case differently
    http_response_code(404); // Not Found
    echo json_encode(['message' => 'No posts found', 'totalPosts' => $totalPosts]);
} else {
    // Return JSON response including totalPosts
    echo json_encode(['posts' => $posts, 'totalPosts' => $totalPosts]);
}

// Close database connection
mysqli_close($conn);
```

## Single Post Display & Like/Dislike Feature

```javascript
import React, { useState } from "react";
import { useParams } from "react-router-dom";
import axios from "axios";

const Post = () => {
    const { id } = useParams();
    const [post, setPost] = useState(null);
    const [likeCount, setLikeCount] = useState(0);
    const [dislikeCount, setDislikeCount] = useState(0);
    const [ipAddress, setIpAddress] = useState("");

    const fetchPost = async () => {
        try {
            const response = await axios.get(`${process.env.REACT_APP_API_BASE_URL}/post.php/${id}`);
            const post = response.data.data;
            setPost(post);
            setLikeCount(post.likes);
            setDislikeCount(post.dislikes);
        } catch (error) {
            console.log(error);
        }
    };

    const fetchIpAddress = async () => {
        try {
            const response = await axios.get("https://api.ipify.org/?format=json");
            setIpAddress(response.data.ip);
        } catch (error) {
            console.log(error);
        }
    };

    const handleLike = async () => {
        try {
            const response = await axios.post(`${process.env.REACT_APP_API_BASE_URL}/post.php/${id}/like/${ipAddress}`);
            const likes = response.data.data;
            setLikeCount(likes);
        } catch (error) {
            console.log(error);
        }
    };

    const handleDislike = async () => {
        try {
            const response = await axios.post(`${process.env.REACT_APP_API_BASE_URL}/post.php/${id}/dislike/${ipAddress}`);
            const dislikes = response.data.data;
            setDislikeCount(dislikes);
        } catch (error) {
            console.log(error);
        }
    };

    React.useEffect(() => {
        fetchPost();
        fetchIpAddress();
    }, []);

    if (!post) {
        return <div>Loading...</div>;
    }

    return (
        <div className="container my-4">
            <h1 className="mb-4">{post.title}</h1>
            <p>{post.content}</p>
            <hr />
            <div className="d-flex justify-content-between">
                <div>
                    <button className="btn btn-outline-primary me-2" onClick={handleLike}>
                        Like <span className="badge bg-primary">{likeCount}</span>
                    </button>
                    <button className="btn btn-outline-danger" onClick={handleDislike}>
                        Dislike <span className="badge bg-danger">{dislikeCount}</span>
                    </button>
                </div>
                <div>
                    <small className="text-muted">
                        Posted by {post.author} on {post.date}
                    </small>
                </div>
            </div>
        </div>
    );
};

export default Post;
```

```javascript
// Load configuration files
require_once('../config/config.php');
require_once('../config/database.php');

if ($_SERVER['REQUEST_METHOD'] === 'GET') {
    $requestUri = $_SERVER['REQUEST_URI'];
    $parts = explode('/', $requestUri);
    $id = end($parts);

    $query = "SELECT bp.*, 
                     (SELECT COUNT(*) FROM post_votes WHERE post_id = bp.id AND vote_type = 'like') AS numLikes,
                     (SELECT COUNT(*) FROM post_votes WHERE post_id = bp.id AND vote_type = 'dislike') AS numDislikes
              FROM blog_posts AS bp WHERE bp.id = ?";

    $stmt = $conn->prepare($query);
    $stmt->bind_param('i', $id);
    $stmt->execute();
    $result = $stmt->get_result();

    if ($result->num_rows === 1) {
        $post = $result->fetch_assoc();

        $response = [
            'status' => 'success',
            'data' => [
                'id' => $post['id'],
                'title' => $post['title'],
                'content' => $post['content'],
                'author' => $post['author'],
                'date' => date("l jS \of F Y", strtotime($post['publish_date'])),
                'likes' => $post['numLikes'],
                'dislikes' => $post['numDislikes']
            ]
        ];

        header('Content-Type: application/json');
        echo json_encode($response);
    } else {
        $response = [
            'status' => 'error',
            'message' => 'Post not found'
        ];

        header('Content-Type: application/json');
        echo json_encode($response);
    }

    $stmt->close();
    $conn->close();
}

function checkVote($conn, $postId, $ipAddress, $voteType) {
    $query = "SELECT * FROM post_votes WHERE post_id=? AND user_ip=? AND vote_type=?";
    $stmt = mysqli_prepare($conn, $query);
    mysqli_stmt_bind_param($stmt, "iss", $postId, $ipAddress, $voteType);
    mysqli_stmt_execute($stmt);
    $result = mysqli_stmt_get_result($stmt);
    return mysqli_num_rows($result) > 0;
}

function insertVote($conn, $postId, $ipAddress, $voteType) {
    if (!checkVote($conn, $postId, $ipAddress, $voteType)) {
        $query = "INSERT INTO post_votes (post_id, user_ip, vote_type) VALUES (?, ?, ?)";
        $stmt = mysqli_prepare($conn, $query);
        mysqli_stmt_bind_param($stmt, "iss", $postId, $ipAddress, $voteType);
        mysqli_stmt_execute($stmt);
        return mysqli_stmt_affected_rows($stmt) > 0;
    }
    return false;
}

function removeVote($conn, $postId, $ipAddress, $voteType) {
    if (checkVote($conn, $postId, $ipAddress, $voteType)) {
        $query = "DELETE FROM post_votes WHERE post_id=? AND user_ip=? AND vote_type=?";
        $stmt = mysqli_prepare($conn, $query);
        mysqli_stmt_bind_param($stmt, "iss", $postId, $ipAddress, $voteType);
        mysqli_stmt_execute($stmt);
        return mysqli_stmt_affected_rows($stmt) > 0;
    }
    return false;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $segments = explode('/', $_SERVER['REQUEST_URI']);
    $postId = $segments[6];
    $action = $segments[7];
    $ipAddress = $segments[8];
    $voteType = $action === 'like' ? 'like' : 'dislike';

    if (checkVote($conn, $postId, $ipAddress, $voteType)) {
        if (removeVote($conn, $postId, $ipAddress, $voteType)) {
            http_response_code(200);
            echo json_encode(['message' => ucfirst($voteType) . ' removed successfully.']);
        } else {
            http_response_code(500);
            echo json_encode(['message' => 'Failed to remove ' . $voteType . '.']);
        }
    } else {
        if (insertVote($conn, $postId, $ipAddress, $voteType)) {
            http_response_code(201);
            echo json_encode(['message' => ucfirst($voteType) . ' added successfully.']);
        } else {
            http_response_code(500);
            echo json_encode(['message' => 'Failed to add ' . $voteType . '.']);
        }
    }
}
```

## Navbar

```javascript
import React from 'react';
import { Link } from 'react-router-dom';

const Navbar = () => {
    return (
        <nav className="navbar navbar-expand-lg navbar-light bg-light">
            <div className="container-fluid">
                <Link className="navbar-brand" to="/">Blog Application</Link>
                <button
                    className="navbar-toggler"
                    type="button"
                    data-bs-toggle="collapse"
                    data-bs-target="#navbarNav"
                    aria-controls="navbarNav"
                    aria-expanded="false"
                    aria-label="Toggle navigation"
                >
                    <span className="navbar-toggler-icon"></span>
                </button>
                <div className="collapse navbar-collapse" id="navbarNav">
                    <ul className="navbar-nav">
                        <li className="nav-item">
                            <Link className="nav-link" to="/">Home</Link>
                        </li>
                        <li className="nav-item">
                            <Link className="nav-link" to="/create-post">Create Post</Link>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    );
};

export default Navbar;
```

## App.js and Route

```javascript
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import './App.css';
import Navbar from './components/Navbar';
import CreatePost from './components/CreatePost';
import Post from './components/Post';
import PostList from './components/PostList';

function App() {
  return (
      <div className="App">
        <BrowserRouter>
          <Navbar />
          <Routes>
            <Route path={"/"} element={<PostList />} />
            <Route path="/create-post" element={<CreatePost />} />
            <Route path="/post/:id" element={<Post />} />
          </Routes>
        </BrowserRouter>
      </div>
  );
}

export default App;
```

Congratulations on completing this comprehensive guide to building a blogging site with React and PHP! Now you’ve a good idea to integrate a React frontend with a PHP backend, implementing essential features like CRUD operations and a like/dislike system.

This project not only [enhances your development skills](https://www.mmainulhasan.com/becoming-a-competent-programmer/), but also serves as a solid foundation for future web applications.

Thank you for choosing this tutorial to advance your [web development journey](https://www.webdevstory.com/programming-roadmap/) on how to create a blogging site using React and PHP.

[Get the full React and PHP tutorial for a blogging platform on Code on GitHub.](https://github.com/mmainulhasan/Projects/tree/master/blogging-stie)

***Support Our Tech Insights***

<a href="https://www.buymeacoffee.com/mmainulhasan" target="_blank">![Buy Me A Coffee](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lsm9uucbnw7x9iw0loxr.png)</a>

<a href="https://www.paypal.com/donate/?hosted_button_id=GDUQRAJZM3UR8" target="_blank">![Donate via PayPal button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ipnhbim2ba56kt32zhn3.png)</a>

### Read Next...

{% embed https://dev.to/mmainulhasan/2024-ultimate-guide-to-javascript-interview-questions-and-answers-4jmh %}

{% embed https://dev.to/mmainulhasan/the-definitive-programming-roadmap-from-novice-to-expert-e1k %} 

{% embed https://dev.to/mmainulhasan/debating-the-right-programming-language-focus-here-instead-2mka %}