Assembly

Assembly, documentation

A landing page for an air conditioning business, with two enquiry forms that really send. Everything you need to change is in one file.

1. Quick start

Three steps and the page is live with working forms.

  1. Open fillup.php in a plain text editor. Notepad is fine, Word is not.
  2. Find $TO near the top and put your own email address there.
  3. 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.

  1. The email arrives, and is not in your spam folder.
  2. A file appeared at storage/enquiries-2026-01.csv with your test in it.
  3. 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

index.html the page itself
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.

#SectionWhat it is for
1HeaderSticky. Logo, section links, phone number, one button. Gains a shadow once the page scrolls.
2HeroFull width photograph under a scrim. Headline, two sentences, two buttons.
3Quick formThree fields, sitting on a white card that overlaps the bottom of the hero.
4Trust stripFour short claims. Change them to things that are true of your business.
5ServicesSix cards with drawn icons. No photographs, so it stays quick to load.
6Where we workThree rows, picture and text alternating sides.
7Parallax bandA photograph that scrolls slower than the page, with a short statement over it.
8How a visit worksFour numbered steps joined by a line.
9What is includedA comparison table, three columns, one highlighted.
10ReviewsThree short quotes.
11GalleryNine photographs, one large, all of them open in a popup.
12The full formEight fields beside a photograph.
13QuestionsSix questions that open and close.
14ClosingA second parallax band with one button.
15FooterCopyright 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.

FieldName in the HTMLHero formFull formRequired
Namenameyesyesno
Phonephoneyesyesyes
Emailemailnoyesno
What they needserviceyesyesno
Kind of propertyplacenoyesno
Area or postcodeareanoyesno
Best time to callwhennoyesno
What it is doingmessagenoyesno

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 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/enquiries-2026-01.csv every enquiry, one file per month
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.

LineWhat to put
hostYour outgoing mail server. Whoever gives you email tells you this. Usually smtp.yourdomain.com, smtp.gmail.com or mail.yourhost.com.
port587 in most cases. 465 only if your provider says so.
securetls goes with 587, ssl goes with 465. They come as a pair.
userYour full email address, including the @ part. Not just the bit before it.
passThat 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.

FileSizeWhere it appears
hero1600 x 900Behind the headline
place-home, place-office, place-shop900 x 620The three alternating rows
band1600 x 900The parallax band, section 7
g1 to g91200 x 900The gallery, and the popup
form900 x 700Beside the enquiry form
close1600 x 900The closing band
top144 x 144The back to top button

Swapping a picture

  1. Crop your photograph to the size in the table above.
  2. Save it over the old file, keeping the same name.
  3. If you cannot make a WebP, delete the matching .webp file 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.


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>

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.

FeatureWith JavaScriptWithout
Both formsSent in the background, the form is replaced by a thank youPosts normally, send.php answers with a page of its own
Field errorsPrinted under the field that is wrongListed on the answer page
QuestionsOpen and closeOpen and close, because they are <details>
GalleryOpens a popupPhotographs still show, they simply do not enlarge
ParallaxThe photograph driftsThe photograph sits still
Plan buttonsPreselect the job and scrollJump to the form
Header shadow, back to topAppear on scrollNot 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.


14. Troubleshooting

What you seeWhat it is
The page is blank after editing fillup.phpA missing quote mark or semicolon. Undo your last change. Use straight quotes, not curly ones.
The form says it sent, but no email cameCheck 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 spamSet $FROM to an address on your own domain, then turn on SMTP. Section 8.
Clicking a plan button does not choose the right jobThe data-service wording no longer matches the start of any option in the list. Section 5.
The page jumps about while it loadsAn image whose width and height attributes do not match the real file. Section 9.
An enquiry from a real person never arrivedLook in storage/trapped-*.csv. If they are in there, they were caught by a spam trap.
The CSV file downloads in a browserStop and fix this. Section 7.
The gallery popup opens on the wrong photographTwo 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