In 1998, every serious website had a guestbook. It was the measure of technical credibility - proof your site was dynamic, that it responded to visitors. Static HTML was for amateurs. A working guestbook meant you understood CGI.
The stack: Perl 5 on a Unix server (Apache on Linux or Solaris), the CGI specification for server-browser communication, and a flat text file for storage. No MySQL for most of us - database hosting cost $40-$80/month extra on top of basic shared hosting. Flat files worked perfectly well for a guestbook receiving dozens of visitors per day.
How CGI Actually Worked
CGI - Common Gateway Interface - was a standard for web servers to execute programs and return the output to the browser as the HTTP response. The mechanism: server receives a POST or GET request, executes the specified program (your Perl script), passes form data via environment variables and STDIN, and sends the program's STDOUT back as the HTTP response body.
The critical constraint: each CGI request spawned a new process. Every page load started Perl from scratch, loaded your script, executed it, and died. No persistent memory. No connection pooling. Under load, this was expensive - but for a guestbook getting fifty visitors per day, perfectly acceptable.
The Complete Guestbook Script
#!/usr/bin/perl
# guestbook.pl - Perl 5 CGI guestbook circa 1998
# Deploy to cgi-bin/, chmod 755, chmod 777 the data directory
use strict;
use warnings;
use CGI;
use POSIX qw(strftime);
my $GUESTBOOK_FILE = '/home/mysite/data/guestbook.txt';
my $MAX_ENTRIES = 100;
my $cgi = new CGI;
my $action = $cgi->param('action') || 'view';
if ($action eq 'post') {
handle_post($cgi);
} else {
show_guestbook($cgi);
}
# ---------------------------------------------------------------
sub handle_post {
my ($cgi) = @_;
my $name = $cgi->param('name') || '';
my $email = $cgi->param('email') || '';
my $message = $cgi->param('message') || '';
my $website = $cgi->param('website') || '';
# Strip HTML - no tags allowed in 1998 guestbooks
for my $var ($name, $email, $message, $website) {
$var =~ s/[<>&"']//g;
$var =~ s/\n/ /g; # collapse newlines in single-line fields
}
if (length($name) < 2 || length($message) < 5) {
print_header();
print "<p><font color='red'><b>Error:</b> Name and message are required fields.</font></p>\n";
print "<p><a href='guestbook.pl'>Go back and try again</a></p>\n";
print_footer();
return;
}
my $timestamp = strftime('%B %d, %Y at %H:%M', localtime);
my @entries = read_entries();
unshift @entries, {
name => $name,
email => $email,
message => $message,
website => $website,
timestamp => $timestamp,
};
# Trim to maximum
@entries = @entries[0 .. $MAX_ENTRIES - 1] if @entries > $MAX_ENTRIES;
write_entries(\@entries);
# Redirect using meta refresh - HTTP 302 redirects were finicky in CGI
print "Content-Type: text/html\n\n";
print "<html><head><meta http-equiv='refresh' content='0;url=guestbook.pl'>\n";
print "</head><body>Thank you! <a href='guestbook.pl'>Click here</a> if not redirected.</body></html>\n";
}
# ---------------------------------------------------------------
sub show_guestbook {
my ($cgi) = @_;
print_header();
print <<'FORM';
<h2>Sign Our Guestbook</h2>
<form method="POST" action="guestbook.pl">
<input type="hidden" name="action" value="post">
<table border="0" cellpadding="4" cellspacing="0">
<tr>
<td align="right"><b>Name:</b> *</td>
<td><input type="text" name="name" size="30" maxlength="60"></td>
</tr>
<tr>
<td align="right"><b>Email:</b></td>
<td><input type="text" name="email" size="30" maxlength="80"></td>
</tr>
<tr>
<td align="right"><b>Website:</b></td>
<td><input type="text" name="website" size="30" maxlength="100"></td>
</tr>
<tr>
<td align="right" valign="top"><b>Message:</b> *</td>
<td><textarea name="message" rows="5" cols="45"></textarea></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="Sign Guestbook"></td>
</tr>
</table>
</form>
<hr>
<h2>Guestbook Entries</h2>
FORM
my @entries = read_entries();
if (!@entries) {
print "<p><i>No entries yet - be the first to sign!</i></p>\n";
}
for my $entry (@entries) {
print "<div style='border-bottom:1px solid #cccccc; padding:6px 0; margin-bottom:6px;'>\n";
print "<b>" . $entry->{name} . "</b>";
if ($entry->{website}) {
print " — <a href='" . $entry->{website} . "'>" . $entry->{website} . "</a>";
}
print "<br>\n";
print "<small><i>" . $entry->{timestamp} . ":</i></small><br>\n";
print $entry->{message} . "\n";
print "</div>\n";
}
print_footer();
}
# ---------------------------------------------------------------
sub read_entries {
my @entries;
return @entries unless -f $GUESTBOOK_FILE;
open(my $fh, '<', $GUESTBOOK_FILE) or die "Cannot open guestbook: $!";
my $entry = {};
while (my $line = <$fh>) {
chomp $line;
if ($line =~ /^NAME:(.*)$/) { $entry->{name} = $1 }
elsif ($line =~ /^EMAIL:(.*)$/) { $entry->{email} = $1 }
elsif ($line =~ /^MSG:(.*)$/) { $entry->{message} = $1 }
elsif ($line =~ /^SITE:(.*)$/) { $entry->{website} = $1 }
elsif ($line =~ /^TIME:(.*)$/) { $entry->{timestamp} = $1 }
elsif ($line eq '---') {
push @entries, $entry if $entry->{name};
$entry = {};
}
}
close($fh);
return @entries;
}
sub write_entries {
my ($entries) = @_;
open(my $fh, '>', $GUESTBOOK_FILE) or die "Cannot write guestbook: $!";
for my $e (@$entries) {
print $fh "NAME:" . $e->{name} . "\n";
print $fh "EMAIL:" . $e->{email} . "\n";
print $fh "MSG:" . $e->{message} . "\n";
print $fh "SITE:" . $e->{website} . "\n";
print $fh "TIME:" . $e->{timestamp} . "\n";
print $fh "---\n";
}
close($fh);
}
sub print_header {
print "Content-Type: text/html\n\n"; # The blank line after headers is mandatory
print "<html>\n<head><title>Our Guestbook</title></head>\n";
print "<body bgcolor='#FFFFFF' text='#000000' link='#0000CC' vlink='#551A8B'>\n";
print "<h1>Welcome to Our Guestbook</h1>\n";
}
sub print_footer {
print "<hr><p><small>Powered by Perl CGI • Hosted on Apache/Linux</small></p>\n";
print "</body></html>\n";
}
Deployment in 1998: FTP, Telnet, chmod
Getting a CGI script running on a shared Unix host in 1998 required:
- FTP the script into the
cgi-bin/directory - web servers only executed CGI from this path - Telnet into the server:
telnet myhost.com 23 - Set executable permissions:
chmod 755 guestbook.pl - Create the data file and make it world-writable:
chmod 777 /home/mysite/data/
That chmod 777 was accepted practice. It was also a security hole - any other user on the shared server could overwrite your data. We knew it was not ideal. We did it anyway, because the alternative required root access to set up proper group permissions, and shared hosting providers gave you neither root nor flexibility.
The most common error: 500 Internal Server Error. Almost always one of three causes:
# Diagnosis sequence every 1998 Perl developer knew cold:
# 1. Wrong Perl interpreter path on shebang line
head -1 guestbook.pl
# Should match: which perl
# 2. Windows line endings (CRLF) on a Unix server - kills the shebang
file guestbook.pl
# If it says "CRLF line terminators", strip them:
perl -pi -e 's/\r\n/\n/g' guestbook.pl
# 3. Permissions wrong
ls -la cgi-bin/guestbook.pl
# Should show: -rwxr-xr-x
# 4. Read the Apache error log (if you had SSH access)
tail -20 /var/log/apache/error.log
What Stopped Spam: Very Little
Spam protection in a 1998 guestbook was minimal:
# Basic rate limiting: check if this IP posted in the last 60 seconds
# Stored in a lock file - crude but effective for 1998 traffic levels
my $ip = $ENV{REMOTE_ADDR} || 'unknown';
my $lockfile = "/tmp/gb_lock_${ip}";
if (-f $lockfile && (time() - (stat($lockfile))[9]) < 60) {
print_header();
print "<p>Please wait before posting again.</p>\n";
print_footer();
exit;
}
# Create/update lock file
open(my $lf, '>', $lockfile) or warn "Cannot create lock: $!";
close($lf);
This failed as soon as anyone had a dynamic IP or used a proxy, which in 1998 was everyone on AOL dial-up. We were not doing much better than nothing. Spam was a manageable problem in 1998 because most spammers had not yet automated web form submission. By 2001, guestbook spam would become severe enough to drive the adoption of CAPTCHA.
Why PHP Displaced Perl for Web Work
By 2000, Perl CGI was rapidly losing ground to PHP 4. The reason was not that PHP was more capable - Perl was arguably more powerful. The reason was deployment friction.
A PHP page:
- Sits next to your HTML files in
public_html/ - Gets executed automatically by Apache's
mod_php - Requires no
cgi-bin/directory - Requires no
chmod 755 - Has the Perl path hardcoded nowhere
The Perl guestbook above required understanding Unix file permissions, the CGI protocol, and Perl's CGI module interface before you could get "Hello World" to the browser. PHP let you write <?php echo "Hello"; ?> in an HTML file and have it work immediately.
That simplicity won the web application market. Perl kept Unix administration, text processing, and bioinformatics. The web went to PHP - and then in 2005, to Rails and the era after that.
The Perl CGI guestbook was the tutorial that taught a generation how web servers communicate with programs. The patterns it demonstrated - parsing form data, generating HTML responses, reading and writing persistent storage - remain exactly the same in every modern web framework, just with the machinery hidden.
Aunimeda builds production-grade backend systems - APIs, microservices, real-time applications, and system integrations.
Contact us for backend engineering services. See also: Custom Software Development, Web Development