The business and what it needed
Midway Junk Removal is a junk removal and hauling business based in the North Carolina Triad. The owner and a small crew clear out houses, garages, rentals and storage units, haul away furniture and appliances, take construction debris and yard waste, do light demolition such as sheds and decks, and help with small moves. They work around the clock by appointment, and the service area runs from Greensboro, Winston-Salem and High Point out to Charlotte and Raleigh.
The owner is a friend of mine. I built the site for him, and I still run it.
His problem was simple to state. People who need junk hauled search for it, usually on a phone, usually with a specific job in mind: an old couch, a garage, a dead refrigerator, a pile of remodel debris. If nothing comes up with his name on it, he does not get the call. He handles every lead himself, by phone and by email, often from the truck between jobs. So the site had one job. It had to let a stranger find the business, see in a few seconds whether it handles their kind of job in their town, and then call or send a short request that lands in his inbox in a form he can act on.
Put in his terms, he wanted a real place online where people looking for junk removal in the Triad could find him, see what he handles and where, and get in touch.
What was there before
There was no working website. The business had a site on a website builder that was started and never finished, and it had since been suspended. What customers could find was a Facebook page with reviews, and the rest came from word of mouth and the phone.
That meant every question a customer had went to the owner by phone. Do you take appliances? Do you come out to Asheboro? Can you do it this weekend? None of it was written down anywhere a search engine could read. The domain the site now runs on was registered when I started the build, at the start of January 2026.
The build
The repository began on January 1, 2026, and the first full version of the site was in place the next day.
The first version
The first version was a single page built with Next.js 14 using the App Router, TypeScript, Tailwind CSS, Zod for validation and SendGrid for email. It was laid out as one long page with sections in the order a customer asks questions: what the business does, photos of recent jobs, how a job works, where it works, operating notes, reviews, common questions, and a contact block with the quote form.
Pieces that were there from the start and are still there now:
- •A quote form with one validation schema shared by the browser and the server, two hidden trap fields for bots, a minimum time before a submission is accepted, and a per-visitor rate limit.
- •A call bar pinned to the bottom of the screen on phones, so the phone number is always one tap away.
- •Local business structured data, so search engines can read the business name, phone number, service area, hours and services.
- •A privacy page, a robots file and a sitemap.
- •An image step using sharp that turned the owner's logo and field photos into two sizes in both JPG and WebP.
The same day, I replaced the placeholder testimonials with real excerpts from the business's Facebook reviews, dated and linked back to the page.
The site was hosted on Vercel from the start. The project lived on a Hobby account in the owner's name, and the code lived in a GitHub organization he owned. Every page was static, and there was no database.
Growing past one page
From May to August 2026 the site grew from one page into a small site, in a series of changes.
- •May 7: upgraded to Next.js 16 and fixed the social sharing metadata so links posted to Facebook show the right image and title.
- •May 10: added a terms page.
- •May 11: fixed the honeypot. Some browsers autofilled the hidden trap field, which could make a real person's request look like a bot. The field now tells browsers not to fill it.
- •May 18: added branded pages for not found, server errors, maintenance and access denied, so a broken link still looks like the business and points back to it.
- •May 28: added thirteen service pages, a services index, a service area page, a contact page and an FAQ page. A single page can only say so much about each service. Giving each kind of job its own page means someone searching for appliance removal or a garage cleanout lands on a page about exactly that.
- •June: added maintainability and bundle size checks to the build, and written manifests that describe what a deployment must have before it goes live and what to check after.
In August I went through the site for accuracy, in three pull requests on August 22.
- •PR #16 corrected stale and unsupported claims across the site and changed the form's configuration error into a message a customer can act on: call directly.
- •PR #18 fixed three bugs. A validation error from the server was returned as an object and rendered straight into the page, which blanked the form. The opening hours in the structured data were free text that search engines could not parse. And the newer pages had no site header at all, so a visitor landing on the FAQ had no way back to the homepage.
- •PR #19 rebuilt the service area from the owner's own promotional flyer, which names 26 towns, and removed insurance, licensing and "municipal jobs" claims that nothing supported.
Why the site moved, and what changed
By late summer every other client site I run lived in the JAMARQ Digital GitHub organization, with the same checks, the same dependency update rules and the same security gate. Midway Junk Removal was the last one outside it. On September 5, 2026 I transferred the repository from the owner's organization into mine. The history came across intact.
The transfer stopped the site from deploying. A Vercel Hobby account cannot deploy a private repository owned by a GitHub organization, and the owner's project was on a Hobby account. The live site kept serving the August 22 build from Vercel's cache, so nothing looked wrong from outside, but no new change could go out.
There were three ways to fix it: upgrade his Hobby account, move the repository back out of the organization, or rebuild the Vercel project on the JAMARQ Digital Pro team. I rebuilt it on the Pro team, the same day. It was the only option that kept the code with the rest of the sites.
The cutover went like this:
- •I created the new project and set its environment variables.
- •The owner removed the domain from his old project, and I added it to the new one. Vercel verified ownership through two TXT records, both on the first try. The site was down for the few minutes between removal and re-adding.
- •I moved the bare domain to the record Vercel recommends, a CNAME at the root of the zone. That is normally not allowed in DNS. It works here because Cloudflare flattens it into A records, which is also why a lookup afterward shows A records and no CNAME.
- •I requested a new certificate covering both the bare domain and www, set to renew automatically.
- •I replaced the SendGrid key with a new one for this site alone, cloned from the existing per-site key's permissions, and confirmed it could not manage keys, domains, billing or users.
The same day I folded the repository's CI into one job, cleared three dependency advisories that had been published since the last green run, and turned on branch protection so nothing reaches main without passing that job. The owner disconnected his old Hobby project on September 15.
What did not change matters as much. The code is the same code. The domain is the same domain. DNS stays in a Cloudflare zone in the owner's own account, and quote requests still land in his inbox.
The rest of September was follow-up work on the new footing:
- •PR #22 added a weekly scheduled advisory sweep.
- •PR #26 corrected the deployment manifests to name the real domain, organization and storage.
- •PR #27 changed the homepage, descriptions and structured data to lead with the Triad and name the wider area out to Charlotte and Raleigh.
- •PR #28 let minor and patch dependency updates merge on their own once every check passes.
- •PR #32 moved CI onto my own self-hosted runners.
- •PR #34 tagged every quote email with the site's name in the shared SendGrid account.
- •PR #35 made the site send quote email only from the production deployment.
- •PR #36 corrected the repository's documentation to describe the site as it actually runs.
- •PR #38 linked every service page from every page that lists services.
- •PRs #40 and #42 added Vercel Web Analytics and updated the privacy page to say exactly what it counts.
The public site, page by page
The site has a homepage, a services index, thirteen service pages, a service area page, an FAQ, a contact page, privacy and terms pages, and branded error pages. Every page is generated at build time. The only code that runs on a server is the endpoint that receives the quote form.
Homepage
The homepage opens with the business's promise in one line: full-service junk removal, demolition and hauling across the Triad and central North Carolina. Under it are two buttons, a call button and a quote button, and a short line covering free estimates, 24/7 availability by appointment and the service area. Beside it is the owner's photo of the trailer staged for a cleanout.
Below that, in order:
- •A card for each of the thirteen services, each linking to its own page.
- •Recent hauls: four of the owner's field photos with short captions.
- •How it works, in three steps: call or request a quote, schedule, and the crew hauls everything away.
- •The service area, with each of the 26 towns shown as a chip.
- •Operating notes: hours, holiday and overnight work, free estimates, and donation and metal recycling before the landfill.
- •Six review excerpts from Facebook, dated, with a link to the rest.
- •Seven common questions in an accordion.
- •The contact block: phone, email, hours, and the quote form.
On a phone the call bar stays pinned to the bottom of the screen the whole way down.
Services index
A grid of cards, one per service page, each with a one-line description. It exists so a visitor, and a search engine, can see the whole list on one page.
The thirteen service pages
Junk removal, furniture removal, appliance removal, garage cleanouts, storage unit cleanouts, rental cleanouts, construction debris, yard waste, demolition, light contracting, moving help, clutter cleanup, and free metal removal.
Every service page follows the same template, filled from one content file:
- •A headline written the way people search for that job, and a short introduction.
- •A call button and a quote button.
- •A panel naming the region and the first eight towns.
- •What this can include: the specific items or situations the service covers, such as couches and sectionals, or refrigerators and freezers.
- •A good fit when: the reasons someone would call, such as the item being too heavy to move alone.
- •Links to every other service.
- •The quote form.
Each page also carries Service structured data that names the business as the provider. The pages are generated from the content file at build time, so adding a service is one new entry in that file.
Service area
A plain statement of where the business works, the full list of 26 towns, and links to every service. The town list comes from the owner's flyer, so the site and his printed material say the same thing.
FAQ
Seven common questions: how pricing works, what can be taken, whether they work nights and weekends, moving help, how far ahead to book, demolition, and what happens to removed items. The page carries FAQ structured data and links to every service.
Contact and the quote form
The contact page is a short heading, a line asking people to call or text for the fastest response, and the form.
The form asks for name, phone, an optional email, the service needed, an optional city, and a description of the job. Name, phone and the description are required. There is no file upload. The copy asks people to text photos to the business number instead, because that is how the owner already works.
When a visitor submits, the form checks the same rules in the browser and on the server, so mistakes show up under the right field before anything is sent. A request that passes lands as one email in the owner's inbox with every field, the time it was sent, and enough technical detail to spot abuse. The visitor sees a message on the page saying the request is in and when to expect a reply, with the phone number for anything urgent. If sending fails for any reason, the message tells them to call directly.
Privacy, terms and error pages
The privacy page says in plain language what the form collects, that SendGrid delivers it and Vercel hosts the site, that Vercel Web Analytics counts page visits without cookies, and that the site stores nothing in the visitor's browser. The terms page covers use of the site, and makes clear that a form request is not a booked appointment or a final price. Missing pages and server errors show branded pages with a way back to the site.
Search and sharing
Every page has its own title, description and canonical address. The sitemap lists every page, and the robots file keeps crawlers out of the form endpoint. The homepage carries local business structured data, with the opening hours written in a format search engines accept, and every link shared to social media shows one of the owner's photos.
Infrastructure: domain, DNS, hosting, email, storage, analytics
- •Domain: midway-junk-removal.com, registered through Cloudflare in January 2026.
- •DNS: a Cloudflare zone in the owner's own account. The bare domain is a flattened CNAME to Vercel, and www is a CNAME to Vercel that redirects to the bare domain. The redirect is also enforced in the site's own configuration. One certificate covers both names and renews automatically.
- •Hosting: a Vercel project on the JAMARQ Digital Pro team, linked to the repository. A merge to main deploys production, and preview deployments are protected from the public.
- •Code: a private repository in the JAMARQ Digital GitHub organization, with branch protection that requires the verify job to pass.
- •Email: the domain has no mailboxes. It is set up in SendGrid as a sending domain only, so quote requests go out from a no-reply address on the domain and land in the owner's existing inbox. Sends are tagged with the site's name in the shared SendGrid account, and the key can send mail and nothing else.
- •Storage: none to speak of. There is no database. The logo, photos and every other asset ship with the site.
- •Analytics: Vercel Web Analytics since September 30, 2026. It counts page visits, sets no cookies and identifies nobody. There is no Google Analytics, no tracking pixel and no third-party embed.
- •Security headers: strict transport security, a content security policy, and headers that stop other sites framing it, stop browsers guessing file types, and switch off browser features it does not use.
Decisions worth explaining
No database and no admin
The site describes a business whose services, towns and hours change a few times a year. A database and an admin would add a login to protect, a backup to run and a monthly bill, all to edit text that rarely changes. The content lives in small typed files in the repository, and the pages are built from them. When the owner wants something changed, he tells me and I make the change. It takes minutes and goes through the same checks as any other change.
Phone first, form second
The owner closes jobs on the phone. Every page that matters has a call button near the top, and on a phone the call bar never leaves the screen. The form is there for people who would rather type, or who are searching at midnight. It asks for as little as possible, and its copy points people back to texting photos, since a photo tells the owner more about a job than a paragraph does.
Spam protection without a CAPTCHA
A CAPTCHA costs real customers time and some of them give up. The form uses checks a person never notices. Two hidden fields catch bots that fill in everything. A minimum time catches scripts that submit instantly, and it is checked in the browser and again on the server. A per-visitor rate limit stops a single source from flooding the inbox.
Email only from production
Preview deployments and local runs could send real email, so a test submission could reach the owner's inbox. Since PR #35 the endpoint sends only when it is running as the production deployment. A separate setting can force sending on or off. When sending is off the request is logged and the visitor still sees the normal confirmation, so the form can be tested end to end without anyone getting an email.
Replies go to the customer
The domain has no mailboxes, so a reply-to address on the domain would bounce. The quote email is sent with the customer's own address as the reply-to whenever they give one, so the owner hits reply and is talking to the customer.
Accuracy is part of the build
A site that says the business is licensed for something it is not, or covers a town it does not, creates a phone call that ends badly. In August I removed every claim the owner had not made, rebuilt the town list from his flyer,. The FAQ and the operating notes say only what he actually offers.
Fixing the indexing gap in the template
In late September, Search Console showed six of the service pages as discovered but not indexed, and two it had never seen at all. The pages existed, but too few other pages linked to them. PR #38 fixed it in the templates: the homepage, every service page, the service area page and the FAQ now render one link per service page from the same data that builds the pages. A new service page is linked everywhere automatically.
Hours a search engine can read
The business works around the clock by appointment. That was written as a sentence in the structured data, which search engines could not use. PR #18 changed it to the standard format for every day, all day, which is what "24/7 by appointment" means to a machine.
Rebuilding the hosting, keeping the repository with the others
When the transfer stopped deploys, moving the repository back would have been the fastest fix. It would also have left one client site outside the checks and update rules the others share. Rebuilding the Vercel project on my team took a few minutes of downtime and kept everything in one place.
Checking which build is actually live
After the cutover I redeployed with the environment variables set and got a green build, and the domain answered with the right page. It was still serving the first deployment, the one built before any environment variable existed, because the second build had been created as a preview and never promoted. The form would have failed on the first real request. I caught it by comparing the deployment ID in the live page's HTML against the build I expected, and fixed it with an explicit production deployment before a customer ever hit the broken form.
Checks that cost almost nothing and gate on what ships
The repository arrived with a security job running on a billed macOS runner for no reason. I folded every check into one job on Linux, then moved it to my own self-hosted runners. That one job runs a secret scan, the security gate, a dependency install, linting, type checks, the build, and the maintainability and bundle size checks. In October, PR #45 changed the security gate to fail only on advisories in code that ships to visitors. Development-only findings are still printed, and they no longer block a content change. The one open exception, for a build-tool-only advisory, is written down and expires on December 31, 2026.
What it does now
The site is live at midway-junk-removal.com. People find it by searching, read the page for their job, and either call or send a request. The owner answers all of them himself, the same way he always has, and he has heard from at least one customer who says he would never have found the business without it.
Day to day:
- •Leads come in by phone and through the quote form. Each form request arrives as one email, and the owner replies straight to the customer.
- •The photos on the site are the owner's own, taken on real jobs.
- •Vercel Web Analytics shows which pages people visit, without cookies.
- •PageSpeed results for desktop and mobile are measured and published on this case study page, with a link to run the test live.
The repository has 91 commits and 47 pull requests. Since September 5, 2026, every change has had to pass the verify job before it could merge.
Ongoing support
Maintenance is mostly hands-off unless something breaks, and most of the routine work happens without anyone touching it.
- •Dependency updates arrive from Dependabot. Minor and patch updates merge on their own once every check passes, and larger ones wait for me.
- •A weekly sweep checks every dependency for new security advisories, so a problem published against unchanged code shows up within a week.
- •The certificate renews automatically.
- •Every change, from a dependency bump to a new sentence, goes through the same checks before it reaches production.
- •Content changes come to me from the owner, and I make them in the content files.
If the owner ever wants to edit the site himself, the content is already separated from the layout in small files, so adding an editing path later would not mean rebuilding the pages.












