A working guestbook - database, form, and safe output - in about an hour.
Shared hosting comes with PHP and MySQL ready to use, so you can go from nothing to a live, database-backed page without installing anything on a server. This guide builds a small guestbook: visitors leave a name and a message, and the page lists the latest entries. Along the way it covers the three security habits every PHP developer needs.
In your control panel, find the MySQL Databases section and:
guestbook. Many hosts add your account name as a prefix, giving something like u12345_guestbook - note the full name.
The database host is usually localhost on shared hosting; your control panel will say if it's different.
Open phpMyAdmin from the control panel, select your database, go to the SQL tab and run:
CREATE TABLE messages (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) DEFAULT CHARSET = utf8mb4;
utf8mb4 makes sure names and messages in any language - and emoji - are stored correctly.
Your site's public files live in a folder such as public_html. Anything in it can potentially be requested by a browser, so put your database credentials one level above it, in a file called config.php:
<?php
// config.php - lives OUTSIDE public_html
return [
'host' => 'localhost',
'name' => 'u12345_guestbook',
'user' => 'u12345_guestuser',
'pass' => 'your-generated-password',
];
If your host doesn't let you write above public_html, put it in a subfolder and block web access to that folder - but most hosts allow it.
Create public_html/db.php:
<?php
$config = require __DIR__ . '/../config.php';
$pdo = new PDO(
"mysql:host={$config['host']};dbname={$config['name']};charset=utf8mb4",
$config['user'],
$config['pass'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
Exceptions mode means database errors stop the script instead of failing silently, and associative fetch mode gives you rows as ['name' => ..., 'body' => ...] arrays.
Create public_html/index.php:
<?php
require __DIR__ . '/db.php';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim($_POST['name'] ?? '');
$body = trim($_POST['body'] ?? '');
if ($name === '' || $body === '') {
$error = 'Please fill in both fields.';
} elseif (mb_strlen($name) > 100 || mb_strlen($body) > 2000) {
$error = 'That message is too long.';
} else {
$stmt = $pdo->prepare('INSERT INTO messages (name, body) VALUES (?, ?)');
$stmt->execute([$name, $body]);
header('Location: index.php'); // stop refresh re-posting
exit;
}
}
$messages = $pdo
->query('SELECT name, body, created_at FROM messages ORDER BY id DESC LIMIT 50')
->fetchAll();
function e(string $text): string {
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php if ($error): ?>
<p><?= e($error) ?></p>
<?php endif; ?>
<form method="post">
<label>Name <input name="name" maxlength="100" required></label>
<label>Message <textarea name="body" maxlength="2000" required></textarea></label>
<button>Sign the guestbook</button>
</form>
<?php foreach ($messages as $m): ?>
<article>
<h2><?= e($m['name']) ?></h2>
<p><?= nl2br(e($m['body'])) ?></p>
<small><?= e($m['created_at']) ?></small>
</article>
<?php endforeach; ?>
</body>
</html>
? placeholders send user input to MySQL separately from the SQL itself, so no input can change what the query does. This one habit eliminates SQL injection.e(), which wraps htmlspecialchars(). Without it, someone could post a <script> tag that runs in every visitor's browser.
Upload db.php and index.php to public_html and config.php to the folder above it, using the control panel's File Manager or an SFTP client such as FileZilla. Visit your domain and sign your own guestbook.
Then, in the control panel:
display_errors should be off in production; errors go to a log instead.password_hash(), password_verify() and sessions.fetch.