/docs
pSEO Wizard documentation
Everything you need to go from your website to published pages: how the wizard works, how to put the pages on your site without writing code, and what to do if something doesn't work.
Last updated: September 2026
/how-it-works
How it works
The wizard has five steps. You can go back and change any step before you pay.
- 01
Paste your website
We read your homepage and a few key pages (services, prices, menu, about, contact) and detect your type of business. Add any key facts you want every page to use.
- 02
Pick a pattern
Choose the shape of your searches, like “{Service} in {City}” or “{Product} for {Audience}”, and a page template.
- 03
Add the values
List the services, cities or audiences to combine. Each combination becomes one page. If you target places, we check that they make sense for how you serve customers.
- 04
Generate
Generate your free page first. When you're happy with it, pay for the rest (10 pages minimum) and the pages are written one by one.
- 05
Publish or download
Send the pages to WordPress, Shopify, Webflow or WooCommerce, or download them as ready-to-upload HTML files.
/html-export
Download your pages
In Step 5, Download Bundle gives you a ZIP file with everything you need to host the pages anywhere:
dist/One ready HTML file per page, with its styling built in, so it looks right without any other files.dist/index.htmlA simple list of all your pages, with links.sitemap.xmlA sitemap you can submit in Google Search Console.data/The same pages as JSON (title, slug, meta description and HTML), for developers.
To put them online
- Unzip the file.
- Upload the files from dist/ to your hosting, ideally into a /blog folder, for example with your host's file manager or FTP.
- Open one page in your browser to check it, then submit sitemap.xml in Google Search Console.
/json
JSON for developers
The download also has your pages as data, so you can serve them from your own app. data/wizard-data.json is an array with one object per page:
| Field | Type | What it holds |
|---|---|---|
title | string | The page title shown in search results, at most 60 characters. |
meta_description | string | The meta description shown under the title in search results. |
h1 | string | The main heading on the page. |
body_html | string | The complete HTML document: head, inlined CSS, structured data and the “Read also” links. Serve it as it is. |
slug | string | The last part of the page's URL, for example plumber-in-camden. |
keywords | object | The values used for this page, one per variable in your pattern. |
status | string | Always “success” for pages in the export. |
locale | string | The page's language code, for example en or ar. |
data/content.json has the same pages with three extra names that some tools expect: content (the same as body_html), plus metaDescription and description (both the same as meta_description).
Example page object (body_html shortened)
{
"title": "Plumber in Camden | Example Plumbing",
"meta_description": "Same-day plumbing in Camden: boiler service from £89 and leak repairs by Gas Safe registered engineers.",
"h1": "Plumber in Camden",
"body_html": "<!DOCTYPE html><html lang=\"en\" dir=\"ltr\"><head>…</head><body>…</body></html>",
"slug": "plumber-in-camden",
"keywords": { "Service": "Plumber", "Area": "Camden" },
"status": "success",
"locale": "en"
}Serve the pages from your app
Send body_html as the whole response for the page's URL. Don't put it inside your own layout: it already has its own head, styles and structured data. Pick the example for your stack:
Static hosting (no framework)
# Put the exported pages under /blog on any static host
unzip your-site-bundle.zip -d export
mkdir -p public/blog
cp export/dist/*.html public/blog/
# /blog/plumber-in-camden.html works now. For /blog/plumber-in-camden,
# turn on "clean URLs" (Netlify, Vercel, Cloudflare Pages) or, on nginx:
location /blog/ {
try_files $uri $uri.html =404;
}Next.js
import pages from '@/data/wizard-data.json';
export async function GET(_req: Request, { params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const page = pages.find(p => p.slug === decodeURIComponent(slug));
if (!page) return new Response('Not found', { status: 404 });
return new Response(page.body_html, {
headers: { 'Content-Type': 'text/html; charset=utf-8' }
});
}Astro
---
import pages from '../../data/wizard-data.json';
export function getStaticPaths() {
return pages.map(page => ({ params: { slug: page.slug }, props: { page } }));
}
const { page } = Astro.props;
---
<Fragment set:html={page.body_html} />Nuxt
import pages from '~~/data/wizard-data.json';
export default defineEventHandler((event) => {
const slug = decodeURIComponent(getRouterParam(event, 'slug') ?? '');
const page = pages.find(p => p.slug === slug);
if (!page) throw createError({ statusCode: 404 });
setHeader(event, 'Content-Type', 'text/html; charset=utf-8');
return page.body_html;
});PHP (Laravel)
use Illuminate\Support\Facades\Route;
Route::get('/blog/{slug}', function (string $slug) {
$pages = json_decode(file_get_contents(storage_path('app/wizard-data.json')), true);
$page = collect($pages)->firstWhere('slug', $slug);
abort_unless($page, 404);
return response($page['body_html'])
->header('Content-Type', 'text/html; charset=utf-8');
});Keep “Read also” links and /blog URLs working
- Links between pages use the URL structure you chose in Step 3: /blog/{slug}, or /{prefix}/blog/{slug} with a language prefix. Serve every page at exactly that path on the website you entered in Step 1, and the links and the page's structured data are right without any changes.
- If your pages live at a different path, don't edit the HTML by hand. Every “Read also” link has a data-pseo-link attribute with the target page's slug, so you can rewrite them all at once:
read-also.js // Each "Read also" link carries data-pseo-link="<slug>" const html = page.body_html.replace( /href="[^"]*"(\s+data-pseo-link="([^"]+)")/g, (_, attr, slug) => `href="/guides/${slug}"${attr}` ); - The HTML files in dist/ link to each other as {slug}.html instead. Upload them together into the same folder (ideally /blog) and they keep working, with or without clean URLs.
- Import every page of a campaign together. A link to a page you didn't import would lead to a 404: the HTML export removes those links for you, and in your own app you can skip links whose slug isn't in your data.
- Don't change a slug after the page is live. If you have to, add a 301 redirect from the old address to the new one.
/publish
Publish without code
Connect your site in Step 5 and the pages are sent for you. You only need a key from your platform once; the steps below show where to find it.
WordPress
Pages arrive as draft posts, so you can check them before they go live.
- In your WordPress admin, open Users → Profile and scroll down to Application Passwords.
- Type a name such as “pSEO Wizard”, click Add New Application Password and copy the password it shows. It's shown only once.
- In Step 5, choose WordPress and enter your site address, your WordPress username and that application password (not your normal login password).
- Click Publish, then open Posts → Drafts in WordPress to review the pages and publish them.
Shopify
Pages are added as published articles in the blog you choose.
- In your Shopify admin, open Settings → Apps and sales channels → Develop apps, and create an app.
- Under Configure Admin API scopes, turn on read_content and write_content, save, then click Install app.
- Open API credentials and copy the Admin API access token. It starts with shpat_.
- In Step 5, choose Shopify, enter your store address (yourstore.myshopify.com) and the token, pick a blog and click Publish.
Webflow
Pages are added as items in a CMS collection and go live the next time you publish your site.
- In the Webflow Designer, create a CMS collection (for example “Guides”) with a Post Body rich text field and a Post Summary text field. Webflow's blog template already has both.
- Open the collection's template page and connect the name and Post Body to your layout.
- Create an API token under Site settings → Apps & integrations → API access.
- In Step 5, choose Webflow, paste the token, pick the site and the collection, click Publish, then publish your site in Webflow.
WooCommerce
Pages are added as draft products, which suits pages about something you sell.
- In your WordPress admin, open WooCommerce → Settings → Advanced → REST API and click Add key.
- Give the key Read/Write permission, then copy the consumer key (ck_…) and consumer secret (cs_…).
- In Step 5, choose WooCommerce, enter your store address and both keys, click Publish, then review the drafts under Products.
Wix
There's no direct Wix connection yet, but you can add the pages by hand.
- Download your pages in Step 5 (see “Download your pages” above).
- In the Wix Editor, add a new page, then add Embed Code → Embed HTML and paste the full contents of one page's .html file.
- Set the page's URL and SEO title in Wix to match. Search engines read embedded HTML less reliably than normal pages, so WordPress, Shopify or Webflow usually rank better.
/pricing
Pricing & payments
You pay once per page. There's no subscription, and every account gets its first page free.
| Pages | Price per page |
|---|---|
| 1–500 | $0.30 |
| 501–1,500 | $0.25 |
| 1,501–3,500 | $0.20 |
| 3,501+ | $0.15 |
- Prices are graduated: each price applies only to the pages in its range. For example, 600 pages cost 500 × $0.30 + 100 × $0.25 = $175.
- Paid orders need at least 10 pages. There are no discounts or coupon codes.
- If you change a paid campaign, the pages that haven't been generated yet go back to your account as credit for your next order.
- Payments are handled by Dodo Payments, our Merchant of Record. It accepts cards and local payment methods and adds any sales tax or VAT at checkout.
/facts
How pages stay accurate
Search engines and customers punish pages that make things up. These rules apply to every page:
Facts come from your website
Prices, services, reviews and numbers come only from your website and the key facts you enter. A number that isn't on your site is removed from the page.
Every page is different
Each page gets its own plan, and finished pages are compared with each other. A page that repeats the others too much is rewritten.
Honest about locations
If customers come to you, we warn you about places that are far away. If you go to your customers, pages say you serve an area, never that you have a branch there.
Published under /blog
Pages go into a /blog folder by default, which keeps a large set of new pages apart from the rest of your site.
/help
Troubleshooting
- WordPress says the login is wrong (401 or 403)
- Use an application password, not your normal password, together with your WordPress username (not your email). Some security plugins block the WordPress REST API; allow it for your user and try again.
- Shopify won't connect
- Use the Admin API access token that starts with shpat_, and give the app the read_content and write_content scopes. After changing scopes, reinstall the app and copy the new token.
- A page shows 404 after uploading
- Check that the file is in the folder the page's URL expects (for example /blog/) and that the URL language prefix you set in Step 3 matches your site.
- Buttons link to the wrong page
- Buttons go to the Target URL from Step 1, or to the matching page on your site (booking, menu, pricing, contact) when there is one. Set the Target URL before generating; for pages you already have, change the link in the page editor in Step 4.
Still stuck? Email support@wizardseo.co and we'll help.