How I created an automatic website with +930 clicks in <3 months

creating an automatic website

Last update:

I am not going to sell you a website in 5 minutes or a motorcycle for a euro and a half. Automation also takes time and costs, although it allows processes to be accelerated and a single professional to cover much more work. In fact, I am going to show you how I have created an automatic website in WordPress all by myself.

Table of Contents

Small reflection on the future of work and AI

For many years, the professional specialty has been a topic of frequent concern for the great thinkers of society. Even classical sociological theorists talked about the danger of professional specialization.

In short, they thought that specialization would make us unaware of what existed outside of our professional reach, and that some entities, such as bureaucracy, would end up having a life of their own and being out of our control. Then, the professionals would be part of an organism that they would not fully understand.

We can say that, today, the paradigm has changed . AI is evolving in an unfathomable way, it has even come to surprise Google and other large companies in the digital world. In addition, new professional profiles are being created that encompass a more horizontal knowledge.

Lately there has been a lot of talk about the type T professional profile, which has a specialty and extensive knowledge of everything that surrounds its main subject. A type of professional for whom I see a great future. In fact, this article is perfect for this type of professional , if you keep reading you will discover why.

Performance values of this website

Although from my point of view they are not the determining factor for the success of a web project, we are going to start with the performance values that I know many of you like, nice graphics and round numbers.

All the values that I show below are from the day I write this article (07/12/23) or the day before.

Pagespeed Insights

The values that we find in this platform are not bad at all, 99 for the mobile version and 100 for the desktop version. The truth is that I could have started to optimize the uploading of files even more, but I said “what for”, so you can see what can be achieved with a good server like Wetopi and a theme that loads fast .

Compare your website’s TTFB with that of a copy hosted on Wetopi

We offer a free migration of your website without any obligation or cost.

Sign up in 10 seconds and get your account with free WordPress migrations.

It must be taken into account that these values have been obtained even using tools that significantly affect performance , such as Microsoft’s Clarity, Google Analytics and Complianz.

Performance 100 in PageSpeed Insights for desktop
Page Speed Insights results of almost all pages.
Result of 100 in PageSpeed Insights for mobile
Google Page Speed result, of one of the pages with the most impressions.

These values are those launched by the page with the most impressions and web visits. The rest of the pages do not have a lower value, there are several that even have 100/100.

Google Search Console

The truth is that this graph is very beautiful, since the day the web was launched it has not stopped growing progressively.

As I think, it still has time to hit the growth spurt, since 50% of the impressions and clicks are taking only 2 pages , some of the initial 250 pages are beginning to position themselves on the 1st page now. There is still a long way to go.

evolution of impressions, clicks and average position in Google Search Console
Results about impressions, CTR and position, displayed in Google Search Console

I have not added the average CTR to make it more visual, but those of you who are used to seeing these graphs I suppose you will get the idea.

Microsoft Clarity

In Clarity I have obtained quite good information to study user interaction. But since I want to show you the crumb, I show you a screenshot of the time that users usually spend on this website .

Clarity Stats on the time users usually spend on this website
Audience results from Microsoft Clarity.

Automatic website in WordPress and without plugins

The use of WordPress has a multitude of advantages. However, in this case, we would need to highlight the extensive customization capabilities that we can access

And now I am going to put my hands in the dough and try to explain in the best possible way how I have created this website.

Technologies used for this project

  • Professional content marketing tools to study Kws and competition
  • Chat GPT API
  • Google Sheets API
  • Leonardo.ai API
  • WordPress, Gutenberg
  • Native WordPress and PHP code
  • WordPress REST API
  • JavaScript (jQuery and jsPDF)
  • Mysql
  • HTML and CSS
  • Keyword combinator
  • Shuffle text lines
  • NAS
  • Bash
  • Python

Steps when creating this automated website in WordPress

I wanted to spend my time planning the project. The steps have been the following:

  1. Kws and competition study
  2. Planning next steps
  3. File preparation in Google Sheets
  4. Prompt testing with the GPT chat API
  5. Target audiences
  6. web design
  7. Creation of a theme in WordPress for the occasion
  8. Create the main content of the web in WordPress with the APIs of chatGPT4, Google Sheets and Leonardo.ai
  9. Create the rest of the content
  10. Registration and membership
  11. final touches
  12. Application to create scheduled automatic articles using a NAS and Python

About the project

This automated website deals with a topic that I considered very appropriate to work with chatGPT for several reasons:

  • There will be no informative errors coming from the AI
  • Automatic content is 99% creative
  • Through a series of implementations we will make it easy for users to interact with the web
  • The search intention of users is very clear
  • We will be able to offer a simple “Link Magnet” and it will probably work well
  • The niche is not exploited by vertical websites
  • 250 URLs (even more) can be created without fear of encountering errors

Although I am more of a Lovecraftian, this project is very Danielle Steel. Yeah, I know, it’s a bit cheesy, but hey, if it works it works…

For obvious reasons I am not going to provide the domain of the web in question, but if you want to learn more about this project I will publish on LinkedIn how it is growing . And when I believe that it is settled in its theme, I will make it public.

Also, I still want to implement various monetization methods and I don’t want to give everyone who sees the web a light bulb.

What I can say about this automatic website is that:

  1. Offer creative content highly searched on Google
  2. This content is shown on the web as an example
  3. The ability to edit creative content is given
  4. There is a box for users to create new creative content using AI
  5. There is a button to send the content generated by WhatsApp
  6. If visitors register, the ability to download the sample, created and/or edited content in PDF is given

As we can see, user interaction is vital in this project. And I think that thanks to this, quite good times are being created on the pages. For example, a page in June has an average interaction time of 10’25” and there are some visits that exceed 40 minutes!

Kws and competition study

I have used several professional keyword research tools. The truth is that I don’t remember how I came to find the root kw from which the whole project was born. I suppose he was investigating a competitor of some other project.

But when I came across it , I was surprised by the fact that there was no website that would answer it vertically . So I downloaded a CSV file with all the keywords that this tool gave me to work on an interesting URL structure.

Planning

With the CSV file I was able to choose the most interesting Kws and those that I saw something of a future for. So I summarized them in 2 groups, 1 main category and a secondary category. That is, all these items belong to two categories.

main category

  • boyfriend
  • bride
  • mother
  • father
  • sister
  • brother
  • friend
  • friend
  • husband
  • women
  • son
  • daughter

secondary category

  • to cry
  • original
  • consent
  • long
  • short
  • for birthday
  • for valentine
  • for goodbyes
  • last
  • pretty
  • unforgettable
  • romantic
  • for the distance
  • for anniversary
  • not corny
  • virtual
  • sad
  • sincere
  • To ask for forgiveness
  • with pain
  • formal
  • poetic
  • to miss
  • for good morning
  • of reconciliation

File preparation in Google Sheets

The preparation of our Google Sheets is the most important thing in this project, since it will contain the structure of the entire web, as well as the most important prompts of the automated content.

I divided the file into 10 columns to feed the prompts:

  1. Role
  2. kW
  3. secondary kws
  4. main category
  5. Tone
  6. url
  7. secondary category
  8. Number of words
  9. Nouns
  10. Prompt

That is, without the Prompt column, it would be something like this:

Role kW secondary kws towards/cat tone url categories words Nouns
girlfriend cry emotion, darling, love, I love you boyfriend melancholic and affectionate cry to make cry 800 pillow, shirt, color
girlfriend I love you wherever we go, in my dreams, I’ve lost my sanity, you’re like Chinese rice and I’m like chopsticks boyfriend original, creative, loving and fun original original 900 bee, street, puddle
girlfriend my heart entrails, passion, joy, madness boyfriend sentimental, passionate feeling consent 800 water, boot, weather

As you may have already imagined, each line will be an independent URL.

Writing 250 lines of unique content would take weeks. So we can always find automation tools for this . And how did I do it? Very simple, I have entered chatGPT (version 3.5) and I have asked it “Write a complete list of writing tones that chatGPT can interpret” .

I have divided the results into 3 and added them to 3 columns in Keyword Combinator, a tool that allows you to create text strings with the separator that we deem appropriate, in this case the comma.

I’ve then added the result in another tool called Shuffle Text Lines, which will shuffle all the lines to give us a random result. And well, that way we will have all the values for the tone column in less than 10 minutes .

For the secondary Kws column I have done the same but changing the question in chatGPT. This would be: “Write me a list of 100 words that match the main keyword of this project.”

To add even more authenticity to each article I have asked chatGPT to generate a shower of comma separated nouns. And just like for the other columns I have used the Keyword combinator and Shuffle Text Lines tools.

And finally the most boring job, choose the main Kw for each article and review each line one by one to see that everything fits well. This manual work serves to verify that the entire structure of the prompts makes sense, since by placing the tone and the secondary keywords randomly it is very likely that there are inconsistencies.

I have to say that although I added the word count field, chatGPT passed it through the lining.

Prompt testing with the GPT chat API

Before starting the development I spent a good time exclusively to see how the GPT chat API responded to the prompts to find the one that best suited what I wanted.

Wanting everything to be published to Gutenberg automatically, I haven’t found a way to get chatGPT to output the content with Gutenberg’s signature HTML tags and comments.

Also, another problem I found is that it always returned HTML content with <body> and <head> tags.

I have had to solve these little problems in post-processing using PHP.

Creation of a theme in WordPress for the occasion

In addition to the fact that Wetopi‘s servers surprised me in terms of speed, I wanted to go a step further and created a custom theme for this project. Of course, taking into account WordPress security and easy technical maintenance.

Do not break your production site!
Staging environments are the solution!

Clone to a staging environment to test and fix any HTTP error code.
To clone a WordPress site with Wetopi is as easy as a simple click.

I have limited myself to adding the essential files, as well as a couple of page-templates, a folder with the .js files and another with the .css files .

I use page-templates to create the specific structure of each group of pages/posts and to enqueue the specific .js and .css files of each of them. I do the latter using the conditional function is_page_template(). In this way we can segment and remove the CSS that is not used from each specific page of our WordPress.

Target audiences

I am not a marketing expert, and defining the target audience is a task that has always cost me a lot.

Although in this project I had a relatively clear initial idea regarding the users who will access organically, I realized that there were some searches that were outside of that profile .

For example, for the categories boyfriend with feeling , I sensed that the searches would be more for girls and boys between the ages of 14 and 25. But for categories such as missing son , I suppose that the users who did these searches would be mothers and fathers between 30 and 50 years old.

Something that was clear to me is that this website was going to work in South America even better than in Europe. And the truth, for now it seems that it is being like that.

Web design

I have decided to create a small membership in which age and email are requested to access the possibility of downloading the PDF with the sample content, created and/or edited. This is how I am getting the emails for a possible newsletter in the future.

In addition, the design that I created at the beginning is very sober, neutral, I would even say feucho . Once I have a considerable amount of demographic data I will change the design of the web. And well, to say that after 3 months being online, the revised data has surprised me quite a bit:

User age ranking

And I thought that the visitors would be between 14 and 25 years old… Thank goodness I didn’t create a design focused on this audience! .

The graphic details of the web that have not been generated through an external API, such as the logo, the icons of the home, etc. they have been created in SVG for faster loading. To allow SVG to be uploaded to WordPress, you have to configure a couple of things in code, since by default it is not possible.

Create the main content of the web in WordPress with the APIs of chatGPT, Google Sheets and Leonardo.ai

I am going to limit myself to explaining “in prose” how I have developed the plugin to generate the automatic content, because as I put it in a tutorial plan there will not be time for us to eat the grapes. But if you have any questions, I will be happy to answer you through LinkedIn .

Keep in mind that this plugin has been created to be used locally, under an Apache server.

This plugin consists of 3 files with the classes for each API, let’s remember that I have used the APIs of Google Sheets, chatGPT and Leonardo.ai. It has the following structure:

File Structure for the APIs of Google Sheets, ChatGPT and Leonardo.ai
  1. Sheets : This class contains a single method that makes the connection to the API and gets the values from the previously configured Google Sheet
  2. GPT : This class contains:
    • API connection method
    • Method to generate the introduction
    • Method to generate creative content
    • Method to generate the meta description
  3. LEO : In this class I have added several methods:
    • Call for image generation
    • connection method
    • Call to get image url based on the id obtained in the previous method
    • Method to upload image to WordPress from an external URL. This method does seem very useful to me, so I leave it here:
public function up_img( $image_url, $new_name ) {
require_once( ABSPATH . 'wp-admin/includes/file.php' );

        ////Descargamos la imagen a una variable que contiene los datos temporales
        $temporal = download_url( $image_url );

        if( is_wp_error( $temporal ) ) {
            return false;
        }

        //Descargamos el archivo a la carpeta uploads
        $file = array(
            'name'     => basename( $new_name . '.jpg' ),
            'type'     => mime_content_type( $temporal ),
            'tmp_name' => $temporal,
            'size'     => filesize( $temporal ),
        );

        $sideload = wp_handle_sideload(

            $file,
            array(
                'test_form'   => false
            )
        );

        if( ! empty( $sideload[ 'error' ] ) ) {
            return false;
        }

        //Añadimos el archivo a la base de datos para que aparezca en la sección media del panel de control

        $attachment_id = wp_insert_attachment(

            array(
                'guid'           => $sideload[ 'url' ],
                'post_mime_type' => $sideload[ 'type' ],
                'post_title'     => basename( $sideload[ 'file' ] ),
                'post_content'   => '',
                'post_status'    => 'inherit',
            ),

            $sideload[ 'file' ]
        );

        if( is_wp_error( $attachment_id ) || ! $attachment_id ) {
            return false;
        }

        //Actualizamos metadatos
        require_once( ABSPATH . 'wp-admin/includes/image.php' );

        wp_update_attachment_metadata(
            $attachment_id,
            wp_generate_attachment_metadata( $attachment_id, $sideload[ 'file' ] )
        );

        return $attachment_id;
    }

As you have seen in the structure, I also have two files in the control folder and one file in the root folder.

The root file contains nothing more than the configuration and activation files that every plugin has. Thanks to this file we will have a section in the control panel. And this is where the acgpt_callback.php control file comes in:

acgpt_calbback.php includes a form with a simple little button that will be displayed in the plugin section of the WordPress dashboard. This button will call a function housed in the acgpt_form.php file. This last file is where the magic happens, and it does the following:

  1. The Google Sheets API is called and the rows are obtained
  2. A loop is created to treat rows 1 by 1
  3. The data of categories, role, to and Kws is obtained and the introduction is created with the chat API GPT
  4. Creative content is created using the chatGPT API. For this, the data from the Role Sheet, Kws, towards, tone, categories, prompt and nouns are used.
  5. The final content is created with the GPT chat API explaining why it is important to create that creative content
  6. We also create the meta description with the GPT chat API
  7. We create a prompt for the LEO API taking into account the fields role, to and Kw and we save it in a variable
  8. We generate image with the LEO API, get the URL according to the ID and upload it to WordPress
  9. All content generated by chatGPT is filtered and HTML tags defined in the prompt are replaced with Gutenberg comment tags
  10. Step 7 returns the ID of the image already uploaded to our WordPress. It is then when we can create the article in WordPress adding all the generated content:
$post_arr = array(
'post_title'   => $title,

    'post_content' => $final_article,
    'post_status'  => 'publish',
    'post_category' => array('6'),
    'meta_input'   => array(
        '_yoast_wpseo_focuskw'     => $main_kw,
        '_yoast_wpseo_metadesc' => $answer_meta,
    )
);

$post_id = wp_insert_post($post_arr);
$featured = set_post_thumbnail( $post_id, $featured_id );

Python application for daily automatic publishing in WordPress

Although the previous plugin could have been built in Python and might have been even easier, I decided to do it all in WordPress without having to resort to external tools.

To create an application that publishes on a scheduled basis in the way I wanted, I had to resort to the use of external technologies. In this case I have used a NAS, a Python script that uploads the data using the WordPress REST API and programming the task on the NAS using BASH.

I share the code of the control file of this application:

import openai
import base64
import requests
import json
import pandas as pd
from datetime import datetime
from datetime import timedelta
import sendinblue as sb
import get_img_leo as leo
import up_img_wp as up_img

#df = pd.read_csv('../py/articulos2.csv', usecols=[i for i in range(0,3) ], encoding="ISO-8859-1", sep=';')
sheet_url = 'https://docs.google.com/spreadsheets/d/iDdeNuEstroGoogleSHeeT/edit#gid=0'
url_1 = sheet_url.replace('/edit#gid=', '/export?format=csv&gid=')
df = pd.read_csv(url_1)
today = datetime.today().date()
two_days = today + timedelta(days=2)

two_days_article = 0
counter = 0

for date in df['date']:
    try:
        #print(titulo)
        sheet_date = datetime.strptime(date, "%d-%m-%y").date()
        #check if today has article to post

        if sheet_date == today:

            #img leo
            prompt_leo = df['img'][counter]
            img_leo = leo.img_gen_leo(prompt_leo, 448, 352)

            #upload the image
            img_uploaded = up_img.restImgUL(img_leo)
            img_json = json.loads(img_uploaded)
            img_id = str(img_json['id'])
            img_url = img_json['guid']['raw']

            img_code = '<!-- wp:group {"layout":{"type":"constrained"}' + '} -->\n<div class="wp-block-group"><!-- wp:image {"align":"right","id":' + img_id + ',"sizeSlug":"full","linkDestination":"none"} --><figure class="wp-block-image alignright size-full"><img src="' + img_url + '" alt="imagen impactante para web de piercings" class="wp-image-' + img_id + '"/></figure><!-- /wp:image -->'

            #create post
            openai.api_key = "APIKEY de OPENAI"
            titulo = df['title'][counter]

            #Titulo
            MODEL = "gpt-4"
            response = openai.ChatCompletion.create(
                model=MODEL,
                messages=[

                    {"role": "system", "content": "Eres una experta en copy y redacción para webs de [lo que queramos]"},
                    {"role": "user", "content": "Reescribe el siguiente título, con menos de 60 caracteres, y enfócate en hacerlo lo más atractivo posible: " + str(titulo)},
                ],
                temperature=0.7,
            )

            resp_titulo = response.choices[0].message["content"]
            resp_titulo = resp_titulo.replace('"', '')
            #metadescription
            MODEL = "gpt-4"

            response_meta = openai.ChatCompletion.create(
                model=MODEL,
                messages=[
                    {"role": "system", "content": "Eres una experta en copy y redacción para webs de [lo que queramos]"},
                    {"role": "user", "content": "Escribe una metadescription que no supere los 130 caracteres para un artículo llamado: " + str(titulo)},
                ],
                temperature=0.7,
            )

            metadescription = response_meta.choices[0].message["content"]
            #sections
            MODEL = "gpt-4"

            response = openai.ChatCompletion.create(
                model=MODEL,
                messages=[

                    {"role": "system", "content": "Adopta el rol de copywriter profesional especializada en el mundo de [lo que queramos] y devuelve lo que se te solicite en un único párrafo separando las unidades por comas y sin incluir el titulo"},

                    {"role": "user", "content": "Escribe solamente los títulos de las 5 secciones principales y más relevantes que debería llevar el artículo dirigido a [lo que queramos] con titulo: " + str(titulo)},
                ],
                temperature=0.7,
            )
            sections = response.choices[0].message["content"]

            #content
            MODEL = "gpt-4"

            response = openai.ChatCompletion.create(
                model=MODEL,

                messages=[
                    {"role": "system", "content": "Eres una experta en copy y redacción para webs de [lo que queramos]"},
                    {"role": "system", "content": "Debes crear el contenido en pequeños párrafos para facilitar la lectura"},
                    {"role": "system", "content": "Es vital que el contenido no sea reiterativo ni repetitivo"},
                    {"role": "system", "content": "Debes ser respetuosa en todo momento"},
                    {"role": "user", "content": "Genera un artículo completo en HTML incluyendo exclusivamente las etiquetas: <section>, <h2>, <h3>, <p>, <ul>, <li>, <strong>. Excluyendo etiquetas HTML <h1>. El artículo debe contar con un mínimo de 800 palabras y tiene el titulo '" + resp_titulo + "'. Se deben incluir secciones y subsecciones. El contenido de las secciones no debe ser reiterativo ni repetitivo, las secciones deben ser las siguientes: " + sections + ". Debe ser interesante para el usuario, con una jerarquía de encabezados, H2 y H3 en HTML, así como algún listado ul, acaba todas las frases aunque el texto se exceda del máximo. En este artículo se deben responder las principales dudas que tienen los usuarios en Google sobre este tema concreto, es decir, se deben responder sus preguntas frecuentes. Este artículo solo debe abarcar el tema concreto del que trata el título " + resp_titulo + ", sin abarcar temáticas más generales, aunque estén directamente relacionadas con ese título."},

                ],
                temperature=0.7,
            )

            summary = response.choices[0].message["content"]
            summary = summary.replace('"', '')
            summary = summary.replace('<section>', '<!-- wp:group {"layout":{"type":"constrained"}} --> <div class="wp-block-group">')
            summary = summary.replace('</section>', '</div> <!-- /wp:group -->')
            summary = summary.replace('<p>', '<!-- wp:paragraph --><p>')
            summary = summary.replace('</p>', '</p><!-- /wp:paragraph -->')
            summary = summary.replace('<ul>', '<!-- wp:list --> <ul>')
            summary = summary.replace('</ul>', '</ul> <!-- /wp:list -->')
            summary = summary.replace('<li>', '<!-- wp:list-item --> <li>')
            summary = summary.replace('</li>', '</li> <!-- /wp:list-item -->')
            summary = summary.replace('<h2>', '<!-- wp:heading --> <h2 class="wp-block-heading">')
            summary = summary.replace('</h2>', '</h2> <!-- /wp:heading -->')
            summary = summary.replace('<h3>', '<!-- wp:heading {"level":3} --> <h3 class="wp-block-heading">')
            summary = summary.replace('</h3>', '</h3> <!-- /wp:heading -->')
            summary = summary.replace('</header>', '')
            summary = summary.replace('<header>', '')
            summary = summary.split('<body>', 1)[1]
            summary = summary.split('</body>', 1)[0]
            summary = img_code + summary

            login = 'Alvaro Torres'

            password = 'Clave de Rest API de WordPress'
            url = 'https://nuestraweb.com/wp-json/wp/v2/posts'
            headers = {
                'Authorization': 'Basic ' + base64.b64encode(f"{login}:{password}".encode()).decode(),
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0'
            }

            data = {
                'title': resp_titulo,
                'content': summary,
                'featured_media': img_id,
                'status': 'publish',
                'meta': {
                    '_yoast_wpseo_metadesc': metadescription
                }
            }

            response = requests.post(url, headers=headers, json=data)
            post_json = json.loads(response)
            post_id = post_json['id']
            post_url = post_json['guid']['raw']

            if response.status_code != 201:
                print(response.status_code)
                print('Error al crear el post')

        if sheet_date == two_days:
            two_days_article += 1

    except Exception:
        print("Error general")

    counter += 1

if two_days_article == 0:
    sb.send_mail('Configure the article', '[nuestra web] Articles needs to be configured.\n <a href="https://docs.google.com/spreadsheets/d/NuestRoGooGleSheets/edit#gid=0">CLICK HERE</a>', '2 days left to finish configured articles', 'alvaro@mimail.com', 'Alvaro')

If you want to use it, you must modify your Google Sheet, the Openai API key, your email, replace [whatever we want] with the data of your project and the REST API data of your website.

You will also see that 2 of the imported modules will not sound familiar to you:

import get_img_leo as leo
import up_img_wp as up_img

Actually they are the two files where I have stored the chat classesGPT and Leonardo.ai.

I have saved all the files in a folder on a NAS and then I have created a task that executes the main file using a simple BASH command:

cd /
cd volume1/Python/miproyecto
source env/bin/activate
python control_archivo_python.py

I repeat, if anyone has any doubts, they will only have to write to me on LinkedIn and I will be happy to help them in any way I can.

Final Words

This has been a project in which I have invested a lot of time, especially at the planning level. But keep in mind that for the following projects I already have half the work done.

The results are being better than expected, and although that does not guarantee the real success of the project, I am quite optimistic. Of course, the monetization techniques will not be based exclusively on advertising or sponsored posts , but I am going to include a fairly cheap service that I am convinced can work.


Álvaro Torres, besides having many years of experience in web development, is the founder of WebHeroe, a web development agency specialized in customization on WordPress. He defines himself as a WordPress enthusiast who loves to rack his brains to create new digital projects that make his life a little more challenging

We are techies passionate about WordPress. With wetopi, a Managed WordPress Hosting, we want to minimize the friction that every professional faces when working and hosting WordPress projects.

Not a wetopi user?

Free full performance servers for your development and test.
No credit card required.

See how Wetopi stacks up against your current hosting

Try before you buy.

With no obligation on your part, we’ll migrate a copy of your website:

No hidden small text.
No commitments.
No credit card.