Building client websites with Claude
The full path from an empty laptop to a live client website: local server, WordPress, Claude connected in VS Code, and a custom Elementor widget plugin where every page section is its own widget the client can edit. Followed end to end on one real build — a landing page for a Lithuanian construction company.
The stack & the brief
Five pieces have to exist before Claude is useful. Each has one job. Understand them and every error message later makes sense.
| Piece | Job | Where it lives |
|---|---|---|
| Local server | Runs Apache, PHP and MySQL on your laptop | MAMP (Mac) / Laragon (Windows) |
| WordPress | The site itself — content, users, database | Inside the server's web folder |
| Elementor | Page builder — the client's editing surface | A plugin inside WordPress |
| Your widget plugin | Every page section, as a controllable widget | wp-content/plugins/ — code you own |
| VS Code + Claude | Writes and edits that plugin's code | Opened on the plugins folder |
Sections designed by dragging boxes in Elementor live in the database as unreadable JSON. Claude can't read them, and copying them to the next client means exporting templates and hoping. Sections built as widgets are PHP files: Claude reads and edits them, they copy cleanly to the next project, and the client still gets colour pickers and typography controls in the sidebar. Code for you, drag-and-drop for them.
What "connecting Claude to a website" means
There is no button that plugs Claude into a URL. Claude connects to the files behind the site. Three setups, in order of how often we use them:
| Setup | How Claude reaches it | Used for |
|---|---|---|
| Local files | VS Code open on a folder inside MAMP/Laragon | All development. The default. |
| Remote shell | SSH into the host, or VS Code Remote–SSH | Bugs that only happen on the host. |
| WP-CLI / REST | Claude runs commands against the live install | Content audits, bulk edits, reporting. |
Vilnius construction firm, trading since 2008. Builds private houses, does renovation, interior finishing and roofing. Wants a Lithuanian-language site whose main job is one thing: get a visitor to request an estimate (sąmata).
- Domain: granitostatyba.lt · Local:
localhost:8888/granitostatyba - Language: Lithuanian only at launch, English later
- Pages: landing page, Paslaugos, Objektai, Apie mus, Kontaktai, Privatumo politika
- Client must be able to edit: all headings, body text, prices, project photos, colours and section spacing — without calling us
Install the local server
macOS — MAMP
Free version from mamp.info. MAMP Pro is not needed.
- Web root —
/Applications/MAMP/htdocs - Apache port —
8888 - MySQL port —
8889 - DB login —
root/root - phpMyAdmin —
localhost:8888/phpMyAdmin
Open MAMP → Start Servers → wait for both squares to turn green.
Windows — Laragon
Laragon Full from laragon.org. Ships Apache, MySQL, PHP and a one-click WordPress installer.
- Web root —
C:\laragon\www - Apache port —
80(no port in the URL) - DB login —
root/ (blank) - Clean URLs like
http://granitostatyba.test
Alternatives: XAMPP (C:\xampp\htdocs) if you already know it, or Local by WP Engine on either OS for zero configuration.
Servers won't start — something else holds the port, usually Skype, IIS or a second Apache. MAMP: Preferences → Ports. Laragon: Menu → Apache → Port.
Confirm: http://localhost:8888 (Mac) or http://localhost (Windows) shows a server page, not an error.
Install WordPress locally
Laragon: right-click the tray icon → Quick app → WordPress, name it granitostatyba, skip to step 3. Everyone else — don't do this by hand. Let Claude do it:
Open a terminal in the web root and start Claude:
$ cd /Applications/MAMP/htdocs $ claude
One prompt sets up everything — database, WordPress files, and debugging:
> Set up a new local WordPress site in ./granitostatyba on > this MAMP server: > > 1. Create a MySQL database `dmnt_granitostatyba` with > collation utf8mb4_unicode_ci, using MAMP's mysql at > localhost:8889, user root / root. > 2. Download the latest WordPress and unzip it into > ./granitostatyba. > 3. Create wp-config.php pointing at that database, and > enable WP_DEBUG with logging to wp-content/debug.log > but with display off. > > Done means: http://localhost:8888/granitostatyba loads the > WordPress installer with no database error.
Claude runs the terminal commands, shows you each one, and asks before anything destructive.
Finish in the browser — the only part that needs you. Open
http://localhost:8888/granitostatyba, pick Lietuvių as the site language, and create the admin user with a local-only password. Never put a client's real credentials on your laptop.
That collation is what makes ą č ę ė į š ų ū ž store correctly. Get it wrong here and every Lithuanian heading on the site breaks later.
debug.log is the single most useful thing you can hand Claude. Pasting the last
twenty lines turns guesswork into a direct fix.
Set up VS Code
Install VS Code from
code.visualstudio.com.Install these extensions (Cmd/Ctrl+Shift+X):
- PHP Intelephense — autocomplete and error highlighting
- WordPress Snippets — hook and function shortcuts
- EditorConfig — consistent formatting across the team
Enable the terminal command: Cmd+Shift+P → Shell Command: Install 'code' command in PATH. Windows does this automatically.
Open the plugins folder — not the whole WordPress install. This is the step people get wrong:
$ code /Applications/MAMP/htdocs/granitostatyba/wp-content/plugins
The WordPress root holds ~2,000 core files. Opening it makes Claude slower, noisier, and invites edits to files that get wiped on the next update. Point Claude at code you own.
Connect Claude
Install Node.js 18+ from
nodejs.org. Verify withnode --version.Install Claude Code:
$ npm install -g @anthropic-ai/claude-code $ claude --version
Open the VS Code terminal (Ctrl+`) inside the plugins folder and run
claude. First run installs the VS Code extension and adds a Claude panel to the sidebar.Authenticate with
/login. Use the DMNT account, not a personal one — client work stays on agency billing.Verify. Ask something only a connected Claude could answer:
> List every plugin folder here and tell me which are active, > based on their main plugin file.
Real folder names → connected. Invented names, or a request to paste files → wrong folder, go back to Phase 03.
Commands worth memorising
| Command | Does | When |
|---|---|---|
/init | Scans the project, writes CLAUDE.md | First time in a project |
/permissions | Which commands run without asking | When prompts get repetitive |
/clear | Wipes the conversation, keeps the project | Between unrelated tasks |
/code-review | Reviews your recent changes | After each widget |
/security-review | Security pass over the branch | Before every deploy |
| Shift+Tab | Plan mode — proposes before editing | Anything touching 2+ files |
Plan mode for any multi-file change. Read the plan, correct it, approve. Thirty seconds of reading beats an hour of unpicking twelve wrong edits.
Project context file
Claude reads CLAUDE.md at the start of every session. Anything in it becomes a
standing instruction, so you stop repeating yourself. Run /init, then edit the
draft to look like this:
# UAB „Granito statyba" — granitostatyba.lt ## Environment - Local: MAMP · http://localhost:8888/granitostatyba - WordPress 6.x · PHP 8.2 · Elementor (free) + Hello Elementor child theme - Live: https://granitostatyba.lt — never edited directly ## Architecture - Every landing-page section is a custom Elementor widget in the plugin `dmnt-granito-widgets`. No sections built by hand in the Elementor editor. - Widget classes: widgets/class-{name}-widget.php, one class per file - Shared CSS: assets/css/widgets.css — BEM, prefix .dmnt- ## Conventions - Prefix everything `dmnt_` — functions, hooks, options, CPTs - Text domain: `dmnt-granito` · all UI strings translatable - Site content is Lithuanian — keep ąčęėįšųūž intact, never transliterate - Every widget needs the standard style section: padding, background, heading + body colour and typography, accent colour, button with hover, item gap, alignment — all responsive - Escape all output, clean all input, protect every form - Follow WordPress Coding Standards ## Rules - Never edit WordPress core, Elementor, or the parent theme - Never hardcode text that the client might want to change — it belongs in a control - Ask before adding a Composer or npm dependency
Without it you re-explain the prefix, the escaping rules and the widget structure every session, and output drifts between developers. With it, two DMNT devs on the same plugin produce code that looks like one person wrote it.
Prompting Claude
In this workflow you rarely write code or type terminal commands by hand — you describe the result and Claude does the work. That makes the prompt the real tool. A weak prompt costs an hour of back-and-forth; a strong one gets it right the first time.
The four parts of a strong prompt
What — the exact thing to build or change, named precisely. Not "a section", but "an Elementor widget
dmnt_statsinwidgets/class-stats-widget.php".Where — the file, widget or page it touches, so Claude doesn't guess. Name the path.
Constraints — the rules that apply: prefix, text domain, escaping, "follow the structure of the hero widget", "no new dependencies". Anything in
CLAUDE.mdis applied automatically; add only what's specific to this task.Done means — how you'll judge the result: "the client can change every colour from the Style tab", "activating throws no notices in debug.log".
Bad prompt vs good prompt
| Bad — vague, no target, no constraints | Good — what · where · constraints · done |
|---|---|
| "Make a hero section" | "Create an Elementor widget dmnt_hero in widgets/class-hero-widget.php: eyebrow, heading with tag selector, CTA button, background image. Style tab with colour, typography and responsive spacing controls. Lithuanian defaults from the brief. Done = every visible text and colour editable from the sidebar." |
| "It doesn't work, fix it" | "The services grid shows on desktop but is empty on mobile. Here are the last 20 lines of debug.log: [paste]. Expected: the same four cards stacked one per row. Find the cause, explain it in one sentence, then fix it." |
| "Make it look better" | "Tighten the reviews section: cards in a 3-column grid at 1024px+, 24px gap, quote in 18px italic, name in bold with the accent colour. Use the existing controls — don't add CSS the client can't override." |
| "Add a form" | "Add a quote form widget dmnt_quote: fields vardas, telefonas, el. paštas, žinutė + GDPR checkbox linking to /privatumo-politika. Sends to the email set in a control. Nonce-protected, every input sanitized, honeypot against bots. Done = a submission lands in my inbox and an empty required field shows a Lithuanian error." |
Working rules
- One task per prompt. Six features in one prompt = six half-built features and no idea which broke the page.
- Plan mode first (Shift+Tab) for anything touching more than one file — read the plan, correct it, then approve.
- Paste errors verbatim — the full message from
debug.logor the browser console, never a paraphrase like "it shows some error". - Point at an example — "follow the structure of class-hero-widget.php" beats three paragraphs of description.
- Say what you'll test — when Claude knows "done" means the form email arriving, it writes toward that instead of toward "code that looks finished".
- Iterate, don't restart — if the result is 80% right, name the 20%: "keep everything, but the gap between cards should be a responsive slider control".
/clearbetween unrelated tasks — old conversation context bleeds into new work.
The first three parts tell Claude what to write. The last one tells it when to stop and what to verify. Prompts without a definition of done produce code that looks plausible; prompts with one produce code that works.
Wireframe → widget map
Before any code: list the page's sections top to bottom, and decide which becomes a widget. One section, one widget, one file. Do this on paper or in a text file — it takes fifteen minutes and it determines everything after it.
Which sections become widgets
| Build it as | When |
|---|---|
| Custom widget | The section has a repeating structure, appears more than once, or the client will edit its content — which is nearly always |
| Theme header/footer | Site-wide furniture that never changes per page |
| Native Elementor widget | A plain heading, image or spacer between our sections — don't rebuild what already exists |
Widget name = dmnt_hero. Class = Hero. File =
widgets/class-hero-widget.php. CSS root = .dmnt-hero. Keep those
four in step and any developer can find any section in seconds.
dmnt_ is our agency prefix, not the client's. It goes on
every project we build — Granito statyba included — so our code can never collide with
WordPress, Elementor, or another plugin the client installs later. The client's name
appears in the plugin folder (dmnt-granito-widgets) and in the content;
dmnt_ marks the code as ours.
Theme & Elementor kit
Install Elementor (Plugins → Add New → Elementor).
Choose the theme — two good options:
Theme Pick it when Watch out for Hello Elementor (our default) The whole site is built in Elementor — it's a blank canvas made by the Elementor team, fastest and least likely to fight your widgets It's too blank: it ships almost no styling, so buttons, links, form fields and headings fall back to ugly browser defaults until you define every one of them in the Global Kit (next steps) Twenty Twenty-Five The client will also edit content with the block editor, or you want sensible typography and colours out of the box Its own theme styles sit underneath Elementor's — when a widget looks wrong, you're debugging two style systems instead of one This SOP continues with Hello Elementor.
Create a child theme so theme edits survive updates. Ask Claude:
> Create a Hello Elementor child theme in > ../themes/granito-child — style.css with the correct > Template header, functions.php enqueuing parent then child > styles, and a screenshot placeholder. Text domain granito.
Set the Global Kit in Elementor → Site Settings. This is the brand, defined once; every widget inherits it.
Global token Value Used for Primary #1F2A2Ebetono pilkaHeadings, header, footer Secondary #5A6670Body text Accent #D4610Asignalinė oranžinėButtons, links, numbers Text #2B3134Paragraphs Primary font Barlow Condensed 700 Headings Secondary font Inter 400 Body Override Hello Elementor's defaults. Hello ships with its own fallback colours and sizes that will leak through anywhere the kit is silent. Close every gap while you're in Site Settings:
- Elementor → Settings → General — tick Disable Default Colors and Disable Default Fonts, so the Global Kit always wins over theme defaults
- Site Settings → Typography — set body text and H1–H6 sizes explicitly; Hello's heading sizes are browser defaults
- Site Settings → Buttons — text colour, background, hover colour, border radius and padding; unstyled Hello buttons are grey boxes
- Site Settings → Form Fields — field text, background, border and focus colour, or the quote form inherits bare browser inputs
- Site Settings → Typography → Link colour — normal and hover, or links stay browser blue
Set the container width to 1200px and enable Flexbox Containers in Elementor → Settings → Features. Everything below assumes containers, not the legacy section/column system.
When a widget's colour control is left empty, it falls back to the Global Kit. Change the
accent colour once in Site Settings and every button on the site follows. Hardcode
#D4610A in your CSS instead and you'll be editing seven files the day the
client rebrands.
Avoiding AI slop
Ask for "a modern, clean hero section" and you get the same page every AI has produced for the last two years: purple gradient, centred headline, three identical rounded cards, stock photo of strangers in hard hats. Clients recognise it now. Their customers recognise it too.
For a construction company selling we build things that last, a page that looks mass-produced quietly contradicts the pitch. Slop is not a rendering problem you clean up afterwards — it's what a prompt returns when it wasn't told what to do instead.
The tells, side by side
| Element | AI slop — the default you get | What DMNT ships |
|---|---|---|
| Hero | Centred headline floating over a purple-to-blue gradient, an abstract 3D blob or swoosh behind it | A photo of a house this company actually built, headline set left over it, one clear button |
| Colour | Indigo/violet gradient on white, plus one acid accent, chosen because it looks "tech" | Colours from the client's own world — concrete grey, timber, the orange from their site signage and trucks |
| Type | Inter or Poppins for everything, one weight, heading barely bigger than the body | A deliberate pairing with a real size jump — a heavy condensed heading against quiet body text |
| Imagery | Stock photos of unrelated smiling people in clean hard hats and new hi-vis | The client's own site photos — muddy, real, dated. Imperfect beats generic every time |
| Icons | Emoji, or a generic line icon on every single service card | Photos of the actual work, or one consistent icon set at a weight that matches the type |
| Cards | Four identical rounded boxes, equal shadow, equal spacing, equal importance | Hierarchy — the service that earns the most money is bigger; the rest are secondary |
| Copy | "Empowering your vision with innovative solutions and quality craftsmanship" | "Statome namus Vilniuje nuo 2008 m. 150 baigtų objektų, 5 metų garantija." |
| Trust | Vague badges — "10+ years", "100% satisfaction", five anonymous stars | Named clients, real project addresses, a photo beside each testimonial |
| Spacing | The same padding on everything, uniform rhythm top to bottom | Tight inside a group, generous between sections — spacing that groups related things |
| Motion | Every element fades up on scroll, because that's the default | Motion only where it explains something. Usually that means almost none |
Preventing it in the prompt
All five rules below go into the widget prompts from Part III. They cost one extra paragraph and save a redesign.
Name a direction, never "modern and clean". Those words mean nothing, so Claude fills the gap with the average of everything it has seen. "Heavy, industrial, photo-led, closer to a materials catalogue than a tech startup" gives it somewhere to go.
Feed it the client's world. Their materials, their machinery, their signage, the vocabulary from their quotes. That's where non-generic choices come from.
Ban the defaults out loud. "No gradient backgrounds, no stock photography, no emoji icons, no centred hero, no identical cards." Explicit bans work; hoping doesn't.
Bring real content first. Real Lithuanian copy and real project photos before styling. Lorem and placeholders hide every layout problem until launch day.
Ask for hierarchy, not uniformity. Say which element should be seen first, second, third. Left alone, everything comes out the same size — the visual signature of slop.
Bad prompt vs good prompt
| Produces slop | Produces the brief | |
|---|---|---|
| Hero | "Design a modern, clean hero section for a construction company. Make it look professional and trustworthy." | "Hero for a Vilnius construction firm. Direction: heavy and industrial, closer to a materials catalogue than a tech startup. Full-width photo of a finished house — I'll supply it — heading set left over the lower third, single orange button. Palette from their signage: concrete grey, near-black, one safety orange. Heading heavy and condensed, body quiet and small. No gradients, no stock photos, no centred text. First thing seen is the heading, then the button, then the trust numbers." |
| Services | "Add a services section with four cards and icons." | "Services section, four items. Namų statyba is the main business — give it more visual weight than the other three. Each card uses a photo of that work, not an icon. No equal-size grid, no rounded card shadows. Lithuanian copy from the brief." |
The review pass, before the client sees it
Six questions. A "no" anywhere means go back to the prompt, not to the CSS.
- Cover the logo — is this page still recognisably this company, or could it belong to any firm anywhere?
- Does every photograph come from the client?
- Is there a clear first, second and third in visual weight, or is everything shouting equally?
- Can you say why each colour is on the page, tracing it to something real?
- Does the copy contain a fact a competitor could not honestly copy — a year, a number, an address?
- Is anything on the page there only because pages like this usually have one?
> Review this landing page against the AI-slop checklist in > Phase 09 of our SOP. Be harsh. For each item that fails, > name the widget, say why it reads as generic, and propose > a specific fix drawn from this client's own materials and > photos. Don't change anything yet.
Everything in Part III is fast — Claude builds a widget in minutes. That speed is exactly why the standard has to be set here, before seven widgets exist. Fixing the direction after the fact means rewriting all seven.
Scaffold the plugin
> Create an Elementor widget plugin at > `dmnt-granito-widgets` (name: "DMNT Granito Elements"). > > Requirements: > - Block direct file access, and namespace the code > - Show a friendly admin notice instead of breaking the site > if Elementor is inactive or older than version 3.5 > - Register an Elementor category "Granito statyba" > - Pick up any widget file I add to widgets/ automatically > - Enqueue assets/css/widgets.css on the frontend AND inside > the Elementor editor > - Text domain dmnt-granito, languages/ folder > > Structure only — no widgets yet.
What you should get
dmnt-granito-widgets.php ← header, guards, bootstrap includes/ class-plugin.php ← hooks, category, asset loading widgets/ class-hero-widget.php ← one file per section class-services-widget.php ... assets/ css/widgets.css js/widgets.js languages/ uninstall.php readme.txt
What must be true when it's done
- A Granito statyba category appears in the Elementor widget panel
- Every widget file you add to
widgets/registers itself automatically - The plugin's styles load on the site and inside the Elementor editor preview
- Deactivating Elementor shows a friendly admin notice instead of a broken site
You never need to read the wiring code Claude writes. If any of those four isn't true, describe which one to Claude — it fixes its own plumbing.
If Elementor is deactivated and your plugin isn't guarded, the site white-screens.
Check did_action( 'elementor/loaded' ) before registering anything and show an
admin notice instead of dying.
Activate the plugin. Open any page in Elementor — a Granito statyba category should appear in the widget panel, empty for now.
Anatomy of a widget
You don't type any of this by hand — Claude writes it from a prompt like the one below. But you are the developer of record, so you must be able to read it. Every widget is one class with four jobs: identify itself, declare its controls, render HTML, and map controls to CSS. Walk through the Hero once and the other six are variations.
> Create the first widget: `dmnt_hero` in > widgets/class-hero-widget.php. > > Content tab: eyebrow text, heading (textarea) with a > heading-tag SELECT, CTA text + URL control, background > image. Lithuanian defaults from the brief in CLAUDE.md. > > Style tab: heading colour + typography group, responsive > bottom-spacing slider, responsive section padding > (DIMENSIONS), alignment CHOOSE. Colours default empty so > the Global Kit applies. > > Escape every output. CSS in assets/css/widgets.css under > .dmnt-hero BEM classes. > > Done means: the widget appears under "Granito statyba" in > the Elementor panel and every text and colour is editable > from the sidebar.
What comes back does four jobs. You don't read the code — you check each job in the Elementor editor:
Identity — how the widget introduces itself. Its name, title, icon and category.
Check: it appears in the panel under Granito statyba as "Granito — Hero" with a banner icon.
Content controls — what the client types. One sidebar field for every editable thing: eyebrow, heading, button text and link, background photo.
Check: the Turinys tab shows those fields, already filled with the Lithuanian defaults from the brief.
Style controls — what the client restyles. Colour pickers, typography, spacing sliders and alignment — each wired to one part of the section, with separate values per device.
Check: changing the heading colour changes only this widget's heading, and the tablet / mobile icons next to sliders give separate values.
Render — the HTML visitors receive. Takes whatever the client set in the sidebar and prints it safely on the page.
Check: the section updates live in the preview as you type.
Ask: "Walk me through widgets/class-hero-widget.php section by section in plain language." Ten minutes with that answer and the other six widgets hold no surprises.
"Escape every output" — this is what stops a text field from being used to sneak scripts onto the site. Say it in the prompt; verify it in Phase 19.
"Nothing the client might change is hardcoded" — every visible text, colour and size belongs in a control. If you spot one that isn't, tell Claude to move it into one.
Reload the Elementor editor. The Hero widget now appears under Granito statyba, drags onto the page, and every field in the sidebar edits it live.
Control reference
Every section must be fully editable — text, colour, size, spacing, everything. These are the controls that make that true. Ask for them by name and Claude writes them correctly first time.
Content controls
| Type | Gives the client | Use for |
|---|---|---|
TEXT | Single-line field | Headings, labels, button text |
TEXTAREA | Multi-line field | Subheadings, short descriptions |
WYSIWYG | Full editor with bold/lists/links | Body copy the client formats |
MEDIA | Image picker + media library | Photos, backgrounds, logos |
ICONS | Icon library + SVG upload | Service icons, list bullets |
URL | Link field, new-tab and nofollow toggles | Every button and link |
SELECT | Dropdown | Heading tag, layout variant, column count |
SWITCHER | On/off toggle | Show or hide a sub-element |
NUMBER | Numeric input | Posts to show, items per row |
REPEATER | Add/remove/reorder rows | Services, steps, reviews, stats |
Style controls
| Type | Gives the client |
|---|---|
COLOR | Colour picker, with Global Kit colours listed first |
Group_Control_Typography | Family, size, weight, transform, style, line-height, letter-spacing — all responsive, in one call |
Group_Control_Background | Classic image or gradient, position, size, attachment |
Group_Control_Border | Border type, width per side, colour |
Group_Control_Box_Shadow | Shadow offset, blur, spread, colour, inset |
SLIDER | One value with a unit — gap, height, radius, max-width |
DIMENSIONS | Four linked values — padding or margin |
CHOOSE | Icon button row — alignment, direction |
Three modifiers worth knowing
| Modifier | Effect |
|---|---|
add_responsive_control() | Same control, separate values per desktop / tablet / mobile |
'condition' => [...] | Hides a control until another is set — keeps the sidebar clean |
start_controls_tabs() | Normal / Hover tab pair inside a style section |
The standard style section — every widget gets all eight
- Section padding and margin — responsive
DIMENSIONS - Background —
Group_Control_Background - Heading colour + typography
- Body text colour + typography
- Accent/number colour where the section has one
- Button colour, background, border, radius, padding — with a hover tab
- Gap between items — responsive
SLIDER - Alignment — responsive
CHOOSE
Put that list in CLAUDE.md once, then every widget prompt can just say
"add the standard style section" and Claude generates all eight consistently.
Repeaters & the rest of the sections
Most sections are a list of similar items. That's a repeater: the client adds, removes and reorders rows without touching code.
> Create widget `dmnt_services` — a services grid. > > Content tab: section heading, plus a REPEATER of services — > each row has an icon, pavadinimas and aprašymas. Defaults: > Namų statyba, Renovacija, Vidaus apdaila, Stogo dengimas, > each with a one-line Lithuanian description. > > Style tab: the standard style section, plus card > background / border / radius / hover, and columns per device. > > Escape every output. > > Done means: in the sidebar I can add a fifth service, drag > rows to reorder them, and each row is labelled by its > pavadinimas.
Test it exactly the way the client will use it: add a row, delete a row, drag one to reorder. If any of that feels wrong in the sidebar, say so in one sentence and let Claude adjust.
Build the remaining widgets
One widget per Claude session. Working page after each. The pattern is identical, so this goes fast.
| Widget | Content controls | Style extras |
|---|---|---|
dmnt_stats |
Repeater — skaičius, priedas (+, m²), aprašymas | Number colour + typography separate from label; columns per device |
dmnt_services |
Section heading, repeater of icon/title/text/link | Card background, border, radius, hover lift, columns per device |
dmnt_process |
Repeater — step title + text; switcher for the connecting line | Step number colour and size; line colour; horizontal or vertical SELECT |
dmnt_reviews |
Repeater — citata, vardas, objektas, photo, rating | Quote mark colour, star colour, card shadow |
dmnt_quote |
Heading, field labels, button text, recipient email, consent text + privacy link | Field background, border, focus colour, label typography, button hover |
The form's field labels (Vardas, Telefonas, Žinutė) are TEXT controls, not hardcoded strings. That's what lets the same plugin serve the English version of the site later without a single code change.
Dynamic content widget
The projects section shouldn't be a repeater — the client adds projects as content, not as page design. So: a custom post type plus a widget that queries it.
Register the post type in the same plugin:
> In includes/, register a custom post type `dmnt_objektas` > (labels in Lithuanian: Objektas / Objektai), public, with > title, editor, thumbnail and excerpt. Add a taxonomy > `dmnt_objekto_tipas` (Namas, Renovacija, Apdaila, Stogas). > Add meta fields: vieta (text), metai (number), > plotas_m2 (number) — each one cleaned before saving. > > Done means: an "Objektai" menu appears in wp-admin and I can > add a project with a photo, location, year and floor area.
Build the widget — controls for how it queries, not what it says:
> Create widget `dmnt_projects` that queries the > dmnt_objektas post type. > > Content tab: NUMBER of projects to show, SELECT2 filter by > dmnt_objekto_tipas, SELECT for ordering (date / title / > plotas_m2), SWITCHERs for the vieta, metai and m² badges. > > Style tab: the standard style section from CLAUDE.md. > > Query efficiently — always a fixed limit, never "all posts", > and card thumbnails use a resized image, not the full photo. > > Done means: the widget shows real Objektai posts and the > filter, order and badge toggles all visibly work.
Add three real projects in
wp-adminwith photos. Placeholder content hides layout problems that real content exposes — a project name in Lithuanian is longer than "Project One".
A projects grid loading nine full-size 4MB site photos is the single most common reason a construction site scores badly on mobile. Ask Claude for resized card thumbnails and a fixed query limit — both are in the prompt above for exactly this reason.
Build the landing page
All the engineering is behind you. Assembly takes twenty minutes.
Create a page named Pradžia and set it as the front page in Settings → Reading. Choose the Elementor Canvas or Full Width template.
Edit with Elementor. Open the Granito statyba category — all seven widgets are there.
Drag them in wireframe order: hero, stats, services, process, projects, reviews, quote form. Each lands with sensible Lithuanian defaults already in place, because you set
'default'on every control.Wrap each widget in a Flexbox Container and set the container's width and vertical padding there. Section-level spacing belongs to the container; internal spacing belongs to the widget's own controls.
Give the quote form's container the CSS ID
samataso the hero button's#samatalink scrolls to it.Check every breakpoint with Elementor's responsive preview — desktop, tablet, mobile. Fix problems using the responsive controls you built, not by adding custom CSS.
Hand the page to the client's shoes: change a heading, swap a colour, reorder two services, add a fifth. If any of that needs a developer, a control is missing — go back and add it.
| Section | Text |
|---|---|
| Hero eyebrow | Statybos darbai Vilniuje nuo 2008 m. |
| Hero heading | Statome namus, kurie tarnauja kartoms |
| Hero CTA | Gauti pasiūlymą |
| Stats | 15 metų patirtis · 150+ baigtų objektų · 5 m. garantija |
| Services | Namų statyba · Renovacija · Vidaus apdaila · Stogo dengimas |
| Process | Konsultacija → Sąmata → Statyba → Objekto perdavimas |
| Form heading | Gaukite nemokamą sąmatą per 2 darbo dienas |
| Form fields | Vardas · Telefonas · El. paštas · Trumpai apie projektą |
| Consent | Sutinku, kad mano duomenys būtų tvarkomi pagal privatumo politiką |
The rest of the website
The widgets are page-agnostic. The remaining pages are mostly recombinations.
| Page | Built from |
|---|---|
| Paslaugos | dmnt_services in one-column layout with longer copy, then dmnt_process and dmnt_quote |
| Objektai | dmnt_projects showing all posts, plus Elementor's archive template for single project pages |
| Apie mus | dmnt_stats, dmnt_reviews, and native Elementor text/image widgets |
| Kontaktai | dmnt_quote, a map, and company details |
| Privatumo politika | Plain content page — required, see Phase 17 |
Header and footer
Elementor's free tier has no theme builder, so header and footer live in the child theme's
header.php and footer.php, or come from a header/footer plugin.
Whichever route, the menu is a real WordPress menu the client can reorder — never a hardcoded
list of links.
Įmonės pavadinimas · Įmonės kodas · PVM mokėtojo kodas · registered address · phone in
+370 format · email · link to Privatumo politika. Lithuanian companies are
expected to publish these, and clients notice immediately when they're missing.
Lithuanian & EU requirements
- Charset — database
utf8mb4, page<meta charset="UTF-8">, files saved UTF-8. Test with „Šiaurės Ąžuolų g. 12, Vilnius" — if it renders as question marks, stop and fix the collation before adding content. - HTML lang —
<html lang="lt-LT">; WordPress sets this from the site language. - Phone format — display
+370 6XX XXXXX, link astel:+3706XXXXXXX. - Cookie consent — required under EU rules. Consent must be given before analytics loads, and refusing must be as easy as accepting.
- Privacy policy — a real page naming the data controller, what the form collects, retention period, and the visitor's GDPR rights.
- Form consent — an unticked checkbox linking to the policy. Pre-ticked is not consent.
- Data minimisation — collect only what's needed to send an estimate. No IP logging you can't justify.
- Fonts — self-host, don't hotlink Google Fonts. Serving them from Google's servers has been ruled a GDPR problem in the EU.
- Lithuanian sorting and dates —
Y-m-d, and don't sort Lithuanian names with PHP's default collation. - Accessibility — form labels tied to inputs, visible focus, alt text on project photos in Lithuanian.
Test it end to end before launch: submit, confirm the email arrives at the client's real inbox, check spam, verify the success message is in Lithuanian, and confirm a submission with an empty required field is rejected cleanly. A construction site whose quote form silently fails is worse than no site.
Test & debug
- Every widget drags in cleanly and renders in the editor preview, not just on the frontend
- Every control actually changes something — click through all of them once
- Responsive values work on tablet and mobile independently
- Deleting all repeater rows doesn't fatal the page
- Leaving an image or link empty degrades gracefully
- Lithuanian characters render correctly in the editor, on the frontend, and in form emails
- Deactivating the plugin doesn't white-screen the site — pages lose sections, that's expected
- Deactivating Elementor shows the admin notice, not a fatal error
- Test as Administrator, Editor and Subscriber
- Browser console clean;
debug.logempty - Switch to a default theme — widgets still render
Live log while you test
$ tail -f /Applications/MAMP/htdocs/granitostatyba/wp-content/debug.log
Leave that running. When something breaks the cause appears in real time — copy it to Claude verbatim, file path and line number included.
Security review
Run /security-review in Claude — it checks the technical details. Then ask it
to confirm each line below in plain yes/no terms, and read the answers:
- Nothing prints unescaped — no text from a control reaches the page raw
- The quote form can't be faked — submissions are verified as coming from the real form
- Every form input is cleaned before it's saved or emailed
- The name field can't smuggle email headers into the outgoing message
- Bots are slowed down — a honeypot or rate limit on the form
- Files block direct access when opened outside WordPress
- No passwords or keys in the code — secrets live in
wp-config.php - No debug leftovers — no test output shipping to the live site
> Go through the security checklist in Phase 19 of our SOP > against every widget in this plugin. Answer each line > yes/no with the file that proves it. Fix any "no" and show > me what changed.
A missing nonce, an unescaped echo, or a hardcoded credential blocks the release. Fix, re-run, continue.
Speed & SEO
- Project and hero photos resized and converted to WebP — construction photography is enormous by default
loading="lazy"on everything below the fold; the hero image explicitly not lazy- Widget CSS in one file, enqueued once, no inline
<style>blocks per widget - Caching plugin configured (WP Rocket, LiteSpeed, or the host's own)
- One
<h1>per page — the hero heading; everything else h2/h3 - Title and meta description per page, in Lithuanian, written for people not keywords
- LocalBusiness schema with address, phone, opening hours and service area
- Google Business Profile linked; NAP details identical to the footer
- XML sitemap submitted to Search Console for the
.ltdomain - PageSpeed Insights on mobile — target 90+ before handover
"Add LocalBusiness schema markup to the site footer using the company details in CLAUDE.md, so Google shows our address, phone and opening hours in search results."
Deploy to live
Back up the live site first if one already exists — files and database. Do it even if the host claims automatic backups.
Export the local site. Install All-in-One WP Migration on your local WordPress → All-in-One WP Migration → Export → File. It packages the whole site — pages, widgets, plugin, photos, database — into one
.wpressfile.Prepare the live server. Fresh WordPress install on the host, then install All-in-One WP Migration there too. If the export file is bigger than the upload limit, raise
upload_max_filesizein the host's PHP settings, or use the plugin's file-size extension.Import — All-in-One WP Migration → Import → File, choose the
.wpressfile, confirm. The plugin rewriteslocalhost:8888/granitostatybatogranitostatyba.ltautomatically, including inside Elementor data.Log back in — the import replaced everything, so use your local admin username and password. Then go to Settings → Permalinks and click Save once to refresh the URL rules.
Confirm SSL — the site loads on
https://with a valid padlock. Most hosts issue the certificate in one click.Re-run the Phase 18 checklist on the live domain. Most importantly: submit the real quote form once and confirm the email arrives at the client's inbox.
Never edit plugin files through the WordPress admin file editor on a live site. No undo, no backup. Changes happen locally, then re-deploy.
Keep the .wpress export file — it's your full-site backup. Export a fresh one before every future update, and store one per launch in the client's project folder.
Claude on a live site
Some bugs exist only on the host. Two ways to give Claude access — both read-only until you're certain.
A · SSH into the host
$ ssh dmnt@granitostatyba.lt $ cd ~/public_html/wp-content/plugins/dmnt-granito-widgets $ claude
Or use VS Code's Remote–SSH extension to open the remote folder locally, keeping the Claude panel where you're used to it.
B · WP-CLI for content and database work
$ wp plugin list --status=active $ wp post list --post_type=dmnt_objektas --format=table $ wp search-replace 'http://old.lt' 'https://granitostatyba.lt' --dry-run
Read before you write. Database backup before any write command. --dry-run
first, always. Use a dedicated deploy account, not the client's admin login. Keep
credentials in your SSH config or environment — never in a prompt.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| MAMP won't start | Port in use | Change Apache/MySQL ports in Preferences |
| "Error establishing a database connection" | Wrong host/port | MAMP needs localhost:8889, Laragon localhost |
Lithuanian letters show as ? or Ä… | Database collation isn't utf8mb4 | Recreate the DB with utf8mb4_unicode_ci and reimport |
| Widget missing from the Elementor panel | Class not registered, or wrong category slug | Tell Claude: “the X widget doesn’t appear in the panel” — usually it wasn’t registered |
| Widget renders on frontend but not in the editor | Editor styles not enqueued | Tell Claude: “widget styles load on the site but not in the editor” |
| Style control does nothing | Selector doesn't match the rendered markup | Tell Claude which control does nothing — its CSS target doesn’t match the widget’s markup |
| Styles apply to every instance at once | {{WRAPPER}} missing from the selector | Ask Claude to scope this widget’s style controls to a single instance |
| Fatal after deactivating Elementor | No dependency guard | Ask Claude to add an “Elementor is active” guard with an admin notice |
| White screen after activation | PHP fatal | Read debug.log; rename the plugin folder over SFTP to force deactivation |
| Claude can't see your files | Wrong folder open | Reopen VS Code on wp-content/plugins, restart the session |
| Works locally, breaks live | PHP version mismatch | Ask Claude to compare the two PHP versions and make the plugin work on both |
| Form sends nothing | Local mail isn't configured | Expected on MAMP — test email on the live host, or use an SMTP plugin locally |
Prompt library
New section widget
> Add a new Elementor widget `dmnt_process` to > dmnt-granito-widgets, following the exact structure of > widgets/class-hero-widget.php. > > Content: section heading, and a repeater of steps with > title + description. Lithuanian defaults: Konsultacija, > Sąmata, Statyba, Objekto perdavimas. > > Style: the standard style section from CLAUDE.md, plus > step-number colour and size, connector line colour, and a > horizontal/vertical layout SELECT. > > Escape every output. Add the CSS to assets/css/widgets.css > using .dmnt-process BEM classes.
Add missing controls to an existing widget
> The client can't change the hero button's colours. Add a > Style section for the button with normal/hover tabs: > text colour, background, border group control, border > radius, responsive padding, and typography. Leave defaults > empty so the Global Kit applies.
Debugging a fatal
> The page fatals when the services repeater is empty. Here > are the last 20 lines of debug.log: [paste] > Find the root cause, explain it in one sentence, fix it, > and check the other widgets for the same mistake.
Pre-release review
> Review every widget in this plugin against WordPress Coding > Standards and the security checklist in CLAUDE.md. Flag any > unescaped output, any hardcoded colour or size that should > be a control, and any missing {{WRAPPER}}. List issues by > severity with file and line. Change nothing yet.
Client handover docs
> Write a client guide in Lithuanian explaining how to edit > each section of the landing page in Elementor: which widget > is which, what each control does, and how to add a new > Objektas. Plain language, no technical jargon.
Definition of done
The site is finished when every line below is true. Not before.
- Every wireframe section exists as its own widget
- Every piece of visible text is editable through a control — nothing hardcoded
- Every section has colour, typography and spacing controls, responsive per device
- Client can reorder, restyle and add items without a developer — verified by doing it
- Phase 09 review passed — the page reads as this company, not as a template
- Phase 18 test checklist passes with an empty
debug.log - Phase 19 security checklist passes with no blocking issues
- Phase 17 Lithuanian and GDPR requirements all met
- Mobile PageSpeed 90+; one h1 per page; schema in place
- Quote form tested end to end on the real domain, email received
- A
.wpressexport saved in the client's project folder - Deployed to live and the Phase 18 checklist re-run on the real domain
- Lithuanian client guide delivered; a second DMNT developer has reviewed the diff