1. Quick start
Three steps and the page is live with working forms.
- Open
fillup.phpin a plain text editor. Notepad is fine, Word is not. - Find
$TOnear the top and put your own email address there. - Upload the whole folder to your web host, keeping the folder structure.
Then send yourself one test enquiry from the page and check three things.
- The email arrives, and is not in your spam folder.
- A file appeared at
storage/enquiries-2026-01.csvwith your test in it. - Typing that CSV address into a browser gives 403 or 404, never a download.
Do the third one before you take real enquiries. That folder holds your customers' names, phone numbers and addresses. Section 7 explains what to do if the file downloads.
What you need
A host that runs PHP 7.4 or newer, which is nearly all of them. Nothing to install, no Composer, no libraries, no account anywhere. The page itself is plain HTML and CSS and works on any host at all; only the forms need PHP.
2. What is in the folder
fillup.php your settings and wording. The only file you have to edit.
send.php receives both forms, checks them, saves them, emails them
css/style.css all styling, with every colour set at the top
js/main.js one file, no libraries
images/ every picture on the page, JPEG and WebP
storage/ where enquiries are saved. Read the note inside.
docs/ this documentation
README.txt the short version of this file
The two files you will touch most are index.html for wording on the page and fillup.php for wording in the emails. You should not need to open send.php at all.
3. The sections
Every block in index.html starts with a comment like <!-- ===== 5. Services ===== -->, so searching for the number takes you straight there.
| # | Section | What it is for |
|---|---|---|
| 1 | Header | Sticky. Logo, section links, phone number, one button. Gains a shadow once the page scrolls. |
| 2 | Hero | Full width photograph under a scrim. Headline, two sentences, two buttons. |
| 3 | Quick form | Three fields, sitting on a white card that overlaps the bottom of the hero. |
| 4 | Trust strip | Four short claims. Change them to things that are true of your business. |
| 5 | Services | Six cards with drawn icons. No photographs, so it stays quick to load. |
| 6 | Where we work | Three rows, picture and text alternating sides. |
| 7 | Parallax band | A photograph that scrolls slower than the page, with a short statement over it. |
| 8 | How a visit works | Four numbered steps joined by a line. |
| 9 | What is included | A comparison table, three columns, one highlighted. |
| 10 | Reviews | Three short quotes. |
| 11 | Gallery | Nine photographs, one large, all of them open in a popup. |
| 12 | The full form | Eight fields beside a photograph. |
| 13 | Questions | Six questions that open and close. |
| 14 | Closing | A second parallax band with one button. |
| 15 | Footer | Copyright line and the credit. |
Removing a section
Delete the whole block, from its comment down to the closing </section>. Nothing else breaks, except that any link in the header pointing at it should go too. The one section you cannot delete is number 12, because every button on the page scrolls to it.
4. Colours and fonts
Every colour is a variable at the top of css/style.css. Change one there and it changes everywhere.
:root {
--ink: #0F172A; /* headings and body text */
--ink-soft: #55677A; /* second-rank text */
--blue: #0284C7; /* buttons, links, numbers */
--blue-dk: #026AA2; /* the same, pressed */
--blue-lt: #38BDF8; /* highlights on dark backgrounds */
--tint: #F1F7FB; /* the pale section background */
--line: #DCE7F0; /* hairlines and borders */
--dark: #081828; /* the colour every scrim is mixed from */
}
Changing the main colour. Set --blue to your own, then set --blue-dk to a noticeably darker version of it and --blue-lt to a lighter one. Those two do the work on hover states and on dark backgrounds, so leaving them behind is what makes a recoloured template look wrong.
Fonts
The page uses whatever sans-serif font the reader's computer already has, so there is nothing to download and nothing to slow the page down. To use a web font instead, add its <link> in the head of index.html and change the font line in the body rule.
Rounded corners and width
--r and --r-lg set how round the cards and panels are. Set both to 0 for square corners throughout. --wrap is how wide the content runs, currently 1160 pixels.
5. The services and the plan table
The six service cards
Each one is a small block of HTML in section 5. Copy one to add a seventh, or delete one to have five. The grid rearranges itself either way.
<article class="card">
<span class="card-ico" aria-hidden="true"> ...an SVG icon... </span>
<h3>Servicing and cleaning</h3>
<p>Filters, coils and drain line cleared...</p>
</article>
The icons are drawn in the HTML rather than loaded as pictures, so they are sharp on every screen, they cost nothing to download, and they take their colour from --blue automatically. To change one, replace the <svg> with any other, and give it fill="none" stroke="currentColor" so it picks up the colour.
Keeping the job names in step
This is the mistake people make. A job name appears in three places: the <select> in the hero form, the <select> in the main form, and the data-service attribute on the plan buttons. Rename it in one place only and a button quietly stops selecting the right thing, with no error to warn you. Search index.html for the old wording and change every match.
The comparison table
Section 9 is an ordinary HTML table. Each row is one thing you include:
<tr>
<th scope="row">Written report after the visit</th>
<td><span class="no">No</span></td>
<td class="pl-best"><span class="yes">Yes</span></td>
<td><span class="yes">Yes</span></td>
</tr>
The words Yes and No inside the spans are for screen readers; sighted visitors see a tick or a dash. Keep them.
class="pl-best" marks the highlighted column, and it has to be on every cell of that column including the header, because a table cannot be styled by column any other way. To highlight a different column, move every pl-best across, and move the <span class="pl-flag"> with it.
To drop the prices, delete the <span class="pl-price"> and <span class="pl-sub"> lines from the three header cells, and delete the note underneath the table.
To use four columns rather than three, add a <th> to the header row and a <td> to every body row. Raise the min-width on .plans in the stylesheet by about 200 pixels so the table still scrolls rather than squashes on a phone.
6. The two forms
There are two, and they are the same enquiry. The short one in the hero asks for three things; the long one near the bottom asks for eight. Both post to send.php, both write to the same CSV, and the form column in that file records which one it was.
| Field | Name in the HTML | Hero form | Full form | Required |
|---|---|---|---|---|
| Name | name | yes | yes | no |
| Phone | phone | yes | yes | yes |
email | no | yes | no | |
| What they need | service | yes | yes | no |
| Kind of property | place | no | yes | no |
| Area or postcode | area | no | yes | no |
| Best time to call | when | no | yes | no |
| What it is doing | message | no | yes | no |
Why the phone is the required one. A service visit gets arranged by phone. Asking for an email address as well is one more reason for somebody to give up halfway. If you would rather have the email, open send.php, find the two validation blocks near the comment that says so, and swap which one is allowed to be empty.
The wording in the emails
All of it is in fillup.php, in seven numbered parts. Words in curly brackets are swapped for the real details when the message is sent:
{name} {phone} {email} {service} {place} {area} {when} {message} {site} {date}
Spell them exactly, in lower case. A token that is spelled wrong prints as written rather than vanishing, which is how you spot it.
The customer's copy
It goes out only when the customer typed an email address, which plenty will not. Set $SEND_COPY to false to stop sending it at all.
Spam
Three things run on every submission, and each form has its own pair of hidden fields.
- A honeypot. A field named
website, positioned off the side of the screen. Nobody using the site ever sees it. Anything that fills it in is a robot. - A speed check. The page stamps the time it loaded into the
tsfield. Anything sent within two and a half seconds of that was not typed by a person. - A rate limit. Five submissions an hour from one address, set by
$MAX_PER_HOUR. Addresses are stored hashed, sostorage/rate.jsonholds no personal data.
A caught submission is shown the ordinary thank you rather than an error. Telling a robot it was caught only teaches whoever wrote it what to change. The details go to storage/trapped-*.csv, so a real person caught by mistake is never lost. Look in that file now and then.
Do not delete the hidden fields. Both forms carry website and ts. Remove them and your spam rises sharply within days.
7. Where enquiries are saved
Every enquiry is written to a CSV file before any email is attempted. An email can fail for a dozen reasons outside your control; a written file cannot. The worst an email problem can cost you is a notification, never the job.
storage/trapped-2026-01.csv what the spam traps stopped
storage/mail-failures.log written only when an email did not go out
storage/rate.json counts recent submissions, addresses hashed
Open the CSV in Excel or Google Sheets. It is written with a byte order mark so accented characters come out right.
Keeping the folder private
There is an .htaccess file inside storage/ that blocks web access, written in both the Apache 2.2 and the Apache 2.4 way so it works on either. That covers most shared hosting.
If your host runs nginx, .htaccess is ignored. Do one of these. Either move the storage folder above your web root and point $LOG_DIR in fillup.php at the new place, which is the better answer, or add a deny rule for /storage/ to your nginx config.
Either way, after your site is live, type the address of one CSV file into a browser. You want 403 or 404. If it downloads, your customers' details are public.
8. If email does not arrive
The enquiry is already saved, so nothing is lost. What has failed is only the notification.
PHP's built-in mail function is unreliable on some shared hosting, and mail sent that way often lands in spam because nothing proves it came from you. The fix is to send through your own mailbox instead. Fill in the $SMTP block at the bottom of fillup.php and change 'enabled' to true.
| Line | What to put |
|---|---|
host | Your outgoing mail server. Whoever gives you email tells you this. Usually smtp.yourdomain.com, smtp.gmail.com or mail.yourhost.com. |
port | 587 in most cases. 465 only if your provider says so. |
secure | tls goes with 587, ssl goes with 465. They come as a pair. |
user | Your full email address, including the @ part. Not just the bit before it. |
pass | That mailbox's password. |
Gmail, Outlook and Yahoo will refuse your normal password. They make you create a separate "app password" in your account security settings, and that is what goes in pass. If signing in keeps failing, this is almost always the reason.
Also set $FROM near the top of fillup.php to the same address as user. Many providers refuse a message that claims to come from somewhere else.
The password sits in the file as ordinary text. Use a mailbox created for the website rather than your personal one, and never put a copy of fillup.php anywhere public.
Would you rather use PHPMailer?
It drops in without changing anything else. Replace the body of the send_mail() function in send.php with your PHPMailer calls; every other part of the script carries on as it is. The built-in sender is here so that the template works with nothing installed, not because it is better.
9. Images
Every picture is in images/, twice: a .jpg that works everywhere and a .webp that is roughly half the size. The browser picks the smaller one it understands.
| File | Size | Where it appears |
|---|---|---|
hero | 1600 x 900 | Behind the headline |
place-home, place-office, place-shop | 900 x 620 | The three alternating rows |
band | 1600 x 900 | The parallax band, section 7 |
g1 to g9 | 1200 x 900 | The gallery, and the popup |
form | 900 x 700 | Beside the enquiry form |
close | 1600 x 900 | The closing band |
top | 144 x 144 | The back to top button |
Swapping a picture
- Crop your photograph to the size in the table above.
- Save it over the old file, keeping the same name.
- If you cannot make a WebP, delete the matching
.webpfile and remove its<source>line. The JPEG then does the work on its own.
Keep the width and height attributes matching the real file. They are on every <img> tag. They tell the browser how much room to leave before the picture arrives, and getting them wrong makes the page jump about while it loads.
The photographs are placeholders. They are not covered by the licence on this template. Replace them with your own, or with pictures you have the right to use, before you publish.
The logo
It is drawn in the HTML, not a picture file, so it is sharp everywhere and takes its colour from --blue. Replace the <svg> inside <span class="logo-mark"> with your own, or swap the whole span for an <img> of your logo.
10. Gallery and popup
Nine photographs. The first one takes a two by two square, the rest fill in around it. Every tile is a real <button>, which means it works from the keyboard as well as the mouse.
<button class="gal-item gal-big" type="button" data-cap="What the caption says">
<picture>
<source type="image/webp" srcset="images/g1.webp">
<img src="images/g1.jpg" alt="..." width="1200" height="900" loading="lazy" decoding="async">
</picture>
</button>
data-capis the line printed under the photograph in the popup.gal-bigmakes a tile take the large square. Only put it on one.- Add or remove tiles freely. The popup counts whatever it finds.
Inside the popup, the arrows move between photographs, the arrow keys on the keyboard do the same, Escape closes it, and a swipe works on a phone. While it is open the keyboard stays inside it, and when it closes the focus goes back to the tile you came from.
Why the popup opens instantly. It reuses the file the browser already chose and cached for the tile, rather than fetching a bigger one. If you want a larger picture in the popup, point pic.src in block 6 of js/main.js at a separate file instead.
11. Parallax and motion
The two photograph bands scroll slower than the page. It is done in JavaScript rather than CSS because the CSS way, background-attachment: fixed, does nothing at all on an iPhone.
The picture inside each band is 140% of the band's height and pulled up 20%, which leaves 20% of slack above and below. The script never moves it more than 18% of the band height, so an edge can never appear. To make the movement stronger, raise DRIFT in block 5 of js/main.js, and raise the height and top on .band-pic to match.
To turn parallax off completely, delete the data-parallax attribute from the two <div class="band-pic"> tags. The photographs then sit still and nothing else changes.
People who ask for less movement
Anyone whose computer is set to reduce motion gets no parallax and no smooth scrolling. The page checks prefers-reduced-motion once, at the top of the script, and again in the stylesheet.
12. Without JavaScript
Everything works. This is worth knowing because it is unusual.
| Feature | With JavaScript | Without |
|---|---|---|
| Both forms | Sent in the background, the form is replaced by a thank you | Posts normally, send.php answers with a page of its own |
| Field errors | Printed under the field that is wrong | Listed on the answer page |
| Questions | Open and close | Open and close, because they are <details> |
| Gallery | Opens a popup | Photographs still show, they simply do not enlarge |
| Parallax | The photograph drifts | The photograph sits still |
| Plan buttons | Preselect the job and scroll | Jump to the form |
| Header shadow, back to top | Appear on scroll | Not shown, nothing is lost |
Nothing on the page is hidden by CSS and revealed by script, which is the usual reason a page arrives blank when a script fails to load.
13. Browsers
Chrome, Edge, Firefox and Safari, current versions and a good way back. There is no build step, no framework and no library, so there is nothing to keep up to date.
- WebP images fall back to JPEG on their own.
aspect-ratiokeeps pictures from squashing. An older browser that does not know it still shows the picture.- The layout uses grid, which every browser has supported for years.
- The script is written in the older JavaScript style on purpose, so it runs without being compiled.
14. Troubleshooting
| What you see | What it is |
|---|---|
The page is blank after editing fillup.php | A missing quote mark or semicolon. Undo your last change. Use straight quotes, not curly ones. |
| The form says it sent, but no email came | Check storage/. If the enquiry is in the CSV, the form is fine and the email is the problem. Section 8. |
Nothing appears in storage/ | PHP cannot write there. Set the folder to 755, or 775 if your host needs it. |
| The email arrives in spam | Set $FROM to an address on your own domain, then turn on SMTP. Section 8. |
| Clicking a plan button does not choose the right job | The data-service wording no longer matches the start of any option in the list. Section 5. |
| The page jumps about while it loads | An image whose width and height attributes do not match the real file. Section 9. |
| An enquiry from a real person never arrived | Look in storage/trapped-*.csv. If they are in there, they were caught by a spam trap. |
| The CSV file downloads in a browser | Stop and fix this. Section 7. |
| The gallery popup opens on the wrong photograph | Two tiles carry gal-big. Only one should. |
15. Licence
Use this template for personal, commercial or client work, on as many sites as you like. Do not resell or redistribute the template files themselves.
The photographs that came with it are placeholders and are not covered. Replace them before you publish.
Full terms: 7uptheme.com/license