diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..159462c
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,34 @@
+# Changelog
+
+All notable changes to the WordPress to Wagtail Connector will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+
+- Complete documentation including setup guide, field mapping guide, and troubleshooting guide
+- Make commands for simplifying project setup and management
+- Project structure documentation
+
+### Changed
+
+- Updated documentation to use make commands instead of uv run commands
+- Improved field processor for handling richtext content
+
+### Fixed
+
+- Fixed typos and inconsistencies in documentation
+- Fixed page hierarchy issues with better parent-child relationship handling
+
+## [0.1.0] - 2025-05-21
+
+### Added
+
+- Initial release with basic WordPress to Wagtail migration functionality
+- Support for posts, pages, authors, categories, tags, and comments
+- Django admin interface for managing imports
+- Wagtail integration for viewing imported content
+- Docker setup for WordPress test instance
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..9bbb4c8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,218 @@
+# Makefile for Wagtail-WordPress Connector
+
+# Constants
+WORDPRESS_ROOT = wordpress.docker
+WORDPRESS_URL = http://localhost:8888
+WORDPRESS_API = wp-json/wp/v2
+DC = docker-compose
+
+# Help command
+.PHONY: help
+help:
+ @echo "Wagtail-WordPress Connector Commands"
+ @echo ""
+ @echo "WordPress Container Commands:"
+ @echo " make wp-build - Wordpress: initial setup"
+ @echo " make wp-up - Wordpress: start the container"
+ @echo " make wp-load - Wordpress: import the demo data"
+ @echo " make wp-down - Wordpress: stop the container"
+ @echo " make wp-destroy - Wordpress: destroy the container"
+ @echo ""
+ @echo "Wagtail Virtual Environment Commands:"
+ @echo " make wt-migrate - Wagtail: run migrations"
+ @echo " make wt-superuser - Wagtail: create superuser"
+ @echo " make wt-run - Wagtail: run the server"
+ @echo " make wt-fixtree - Wagtail: fix the tree"
+ @echo ""
+ @echo "Django Import Commands:"
+ @echo " make import-authors - Django: import authors from wordpress"
+ @echo " make import-categories - Django: import categories from wordpress"
+ @echo " make import-tags - Django: import tags from wordpress"
+ @echo " make import-pages - Django: import pages from wordpress"
+ @echo " make import-posts - Django: import posts from wordpress"
+ @echo " make import-media - Django: import media from wordpress"
+ @echo " make import-comments - Django: import comments from wordpress"
+ @echo " make import-all - Django: import all data from wordpress"
+ @echo ""
+ @echo "Node.js Commands:"
+ @echo " make node-setup - Install Node.js dependencies"
+ @echo " make node-build - Build all frontend assets for production"
+ @echo " make node-start - Start the frontend development server"
+ @echo " make node-styles - Compile CSS styles"
+ @echo " make node-styles-watch - Watch and compile CSS styles"
+ @echo " make node-scripts - Compile JavaScript"
+ @echo " make node-scripts-watch - Watch and compile JavaScript"
+ @echo ""
+ @echo "Convenience Commands:"
+ @echo " make start - Run all commands to set up and start the development environment"
+ @echo " make stop - Stop all running services"
+ @echo " make destroy - Destroy and cleanup wordpress and wagtail"
+
+# WordPress Commands
+.PHONY: wp-build
+wp-build:
+ @echo "WordPress: initial setup"
+ @if [ ! -f "$(WORDPRESS_ROOT)/.env" ]; then \
+ cp $(WORDPRESS_ROOT)/.env.example $(WORDPRESS_ROOT)/.env; \
+ fi
+ @mkdir -p $(WORDPRESS_ROOT)/wp-content/plugins
+ @cd $(WORDPRESS_ROOT)/wp-content/plugins && \
+ if [ ! -d "wp-graphql-offset-pagination" ]; then \
+ git clone https://github.com/valu-digital/wp-graphql-offset-pagination.git; \
+ else \
+ echo "Plugin wp-graphql-offset-pagination already exists"; \
+ fi
+
+.PHONY: wp-up
+wp-up:
+ @echo "WordPress: start the container"
+ @cd $(WORDPRESS_ROOT) && $(DC) up -d
+
+.PHONY: wp-down
+wp-down:
+ @echo "WordPress: stop the container"
+ @cd $(WORDPRESS_ROOT) && $(DC) down
+
+.PHONY: wp-destroy
+wp-destroy:
+ @echo "WordPress: destroy the container"
+ @cd $(WORDPRESS_ROOT) && $(DC) down --volumes
+ @echo "Do you want to clean up the files? (y/n)"
+ @read cleanup; \
+ if [ "$$cleanup" = "y" ]; then \
+ if [ -f "$(WORDPRESS_ROOT)/.env" ]; then \
+ rm $(WORDPRESS_ROOT)/.env; \
+ echo "Removed .env file"; \
+ fi; \
+ if [ -d "$(WORDPRESS_ROOT)/wp-content" ]; then \
+ rm -rf $(WORDPRESS_ROOT)/wp-content; \
+ echo "Removed wp-content directory"; \
+ fi; \
+ if [ -d "$(WORDPRESS_ROOT)/xml" ]; then \
+ rm -rf $(WORDPRESS_ROOT)/xml; \
+ echo "Removed xml directory"; \
+ fi; \
+ if [ -f "db.sqlite3" ]; then \
+ rm -rf db.sqlite3; \
+ echo "Removed wagtail database"; \
+ fi; \
+ fi
+
+.PHONY: wp-load
+wp-load:
+ @echo "WordPress: import the demo data"
+ @if [ -z "$$(cd $(WORDPRESS_ROOT) && $(DC) ps | grep wordpress)" ]; then \
+ $(MAKE) wp-up; \
+ fi
+ @cd $(WORDPRESS_ROOT) && $(DC) exec -T wordpress bin/init.sh
+
+# Wagtail/Django Commands
+.PHONY: wt-migrate
+wt-migrate:
+ @echo "Wagtail: run migrations"
+ uv run manage.py migrate
+
+.PHONY: wt-superuser
+wt-superuser:
+ @echo "Wagtail: create superuser"
+ uv run manage.py createsuperuser
+
+.PHONY: wt-run
+wt-run:
+ @echo "Wagtail: run the server"
+ uv run manage.py runserver
+
+.PHONY: wt-fixtree
+wt-fixtree:
+ @echo "Wagtail: fix the tree"
+ uv run manage.py fixtree
+
+# Import Commands
+.PHONY: import-authors
+import-authors:
+ @echo "Django: import authors from wordpress"
+ uv run manage.py import $(WORDPRESS_URL)/$(WORDPRESS_API)/users WPAuthor
+
+.PHONY: import-categories
+import-categories:
+ @echo "Django: import categories from wordpress"
+ uv run manage.py import $(WORDPRESS_URL)/$(WORDPRESS_API)/categories WPCategory
+
+.PHONY: import-tags
+import-tags:
+ @echo "Django: import tags from wordpress"
+ uv run manage.py import $(WORDPRESS_URL)/$(WORDPRESS_API)/tags WPTag
+
+.PHONY: import-pages
+import-pages:
+ @echo "Django: import pages from wordpress"
+ uv run manage.py import $(WORDPRESS_URL)/$(WORDPRESS_API)/pages WPPage
+
+.PHONY: import-posts
+import-posts:
+ @echo "Django: import posts from wordpress"
+ uv run manage.py import $(WORDPRESS_URL)/$(WORDPRESS_API)/posts WPPost
+
+.PHONY: import-media
+import-media:
+ @echo "Django: import media from wordpress"
+ uv run manage.py import $(WORDPRESS_URL)/$(WORDPRESS_API)/media WPMedia
+
+.PHONY: import-comments
+import-comments:
+ @echo "Django: import comments from wordpress"
+ uv run manage.py import $(WORDPRESS_URL)/$(WORDPRESS_API)/comments WPComment
+
+.PHONY: import-all
+import-all: import-authors import-categories import-tags import-pages import-posts import-media import-comments
+ @echo "Imported all WordPress data"
+
+# Convenience Commands
+.PHONY: start
+start: wp-build wp-up wp-load wt-migrate wt-superuser import-all wt-run
+ @echo "Development environment started"
+
+.PHONY: stop
+stop: wp-down
+ @echo "Development environment stopped"
+ @echo "Note: There is no direct equivalent for 'dj stop' and 'wt stop' in the CLI, but WordPress container has been stopped."
+
+.PHONY: destroy
+destroy: wp-destroy
+ @echo "Development environment destroyed"
+
+# Node.js Commands
+.PHONY: node-setup
+node-setup:
+ @echo "Installing Node.js dependencies"
+ @npm install
+
+.PHONY: node-build
+node-build:
+ @echo "Building frontend assets"
+ @npm run build
+
+.PHONY: node-start
+node-start:
+ @echo "Starting the frontend development server"
+ @npm start
+
+.PHONY: node-styles
+node-styles:
+ @echo "Compiling CSS styles"
+ @npm run styles
+
+.PHONY: node-styles-watch
+node-styles-watch:
+ @echo "Watching and compiling CSS styles"
+ @npm run styles:watch
+
+.PHONY: node-scripts
+node-scripts:
+ @echo "Compiling JavaScript"
+ @npm run scripts
+
+.PHONY: node-scripts-watch
+node-scripts-watch:
+ @echo "Watching and compiling JavaScript"
+ @npm run scripts:watch
diff --git a/README.md b/README.md
index f2c3937..de9dfdc 100644
--- a/README.md
+++ b/README.md
@@ -1,75 +1,103 @@
-# Wordess to Wagtail Importer (Experimental)
+# WordPress to Wagtail Importer (Experimental)
+
+[](https://www.python.org/downloads/)
+[](https://wagtail.org/)
+[](https://www.djangoproject.com/)
This is an experimental project to import WordPress content including pages and posts into Wagtail.
-It's not yet ready for production use but a lot of the basic functionality is in place.
+It's not yet ready for production use, but most of the core functionality is in place.
+
+## Features
+
+- Import WordPress pages and posts into a Django application
+- Inspect WordPress API endpoints to understand available data
+- Transfer selected WordPress content to Wagtail
+- Preserve authors, categories, and tags as Wagtail snippets
+- Create redirects from WordPress URLs to new Wagtail URLs
+- Manage imported content through Wagtail's admin interface
## Requirements
-- Python 3.10+ (earlier versions may work)
+- Python 3.13+
- UV & Docker
-- WordPress CLI (instllled via Docker)
-- Wordpress Data (currently using a test data set used for building themes)
-- Wagtail v6.4 (earlier versions may work)
-- Django v5.1 (earlier versions may work)
-- Lots of patience :)
+- WordPress CLI (installed via Docker)
+- WordPress instance with REST API enabled
+- Wagtail 7.0+
+- Django 5.2+
+
+## Workflow Overview
-## Overall Goals
+The migration process follows these steps:
-- To demostrate importing WordPress content into a Django app.
-- Be able to manipulate the imported data using the django-admin.
-- Be able to transfer selected imported wordpress data over to a Wagtail site.
-- Be able to manage the imported data in Wagtail which will have no dependency on the WordPress instance.
-- Once all data is transferred to Wagtail, the WordPress connector app can be removed from the project.
+1. Import WordPress data into Django models
+2. Manage and curate the imported data using the Django admin
+3. Transfer selected content to Wagtail from the Django admin
+4. Manage the transferred content in the Wagtail admin
+5. Remove the WordPress connector app from the project when finished
-
+
-
+
-The overall workflow is as follows:
+### Importing WordPress Data
-1. Import WordPress data into Django
-2. Manage the imported data in the Django admin
-3. Transfer the data to Wagtail from the Django admin
-4. Manage the transferred data in the Wagtail admin
-5. Remove the WordPress connector app from the project
+The importer uses a Django management command to import data from a WordPress instance. While this example imports data from a local WordPress instance, the importer can connect to any WordPress site with the REST API enabled.
-### Importing WordPress data
+To use this for your own site, you'll need to add the `wp_connector` package to your Wagtail project, configure it to point to your WordPress instance, and run the importer.
-The importer will use a Django management command that will import the data from a WordPress instance.
+### WordPress API Inspection
-Although this example imports data from a local WordPress instance, the importer can be used to import data from any WordPress instance that has the JSON api enabled.
+The project includes API inspection tools (`wp_api_inspector.py` and `find_anchor_links.py`) to help you understand the structure of your WordPress data before importing.
-The only package that should be added to your final production site, for importing and transferring the Wordpress Pages and Posts to Wagtail, is the `wp_connector` package. You'll need to add some temporary configuration to Wagtail and then run the importer against your own live WordPress instance, which will need it's JSON api enabled.
+### Transferring Data to Wagtail
-### Transfering data to Wagtail
+The Django admin interface provides a way to select and transfer WordPress content to Wagtail:
-Using the django admin admin interface you will be able to select and transfer the data to Wagtail. Posts and Pages are the main focus for the transfer but linked data such as authors, categories, tags, etc. are also be transferred across. Authors, Categories and Tags are created as snippets. Tags are created within the available Wagtail taggit integration.
+- Pages and Posts are created as corresponding Wagtail page types
+- Authors, Categories, and Tags are created as Wagtail snippets
+- Tags integrate with Wagtail's taggit implementation
+- Redirects are automatically created from WordPress URLs to Wagtail URLs
-The transfer process also includes creating redirects from the old WordPress urls to the new Wagtail urls.
+
-Images and docs linked to and embedded in the transferred pages and posts are also transferred to Wagtail into the Wagtail media library. This action also including updating the links in the content to point to the new Wagtail media urls. (This is not yet implemented)
+### Media Handling (Coming Soon)
-### Completing the transfer
+The transfer process will include handling images and documents:
-Once you have transferred all the data to Wagtail, you can remove the WordPress connector module. This will leave you with a Wagtail site that has no dependency on the WordPress instance. You can then manage the site as you would any other Wagtail site.
+- Media files will be transferred to the Wagtail media library
+- Content references will be updated to point to new Wagtail media URLs
+- *Note: This feature is not yet fully implemented*
-
+### Completing the Transfer
+
+Once you've transferred all your content to Wagtail, you can remove the WordPress connector module. Your Wagtail site will have no dependencies on the WordPress instance, allowing you to manage it like any other Wagtail site.
## Project Setup & Usage
-View the [Setup & Usage Guide](./docs/setup.md) for instructions on setting up the project.
+For detailed setup instructions, see the [Setup & Usage Guide](./docs/setup.md).
-## ToDo's
+## Todo Items
-- Images and Documents are not yet imported
-- Comments are not yet imported
-- and probably lots more I've not yet thought of 😆
+- Complete media import (images and documents)
+- Add comment import functionality
+- Improve error handling and reporting during imports
+- Add more customization options for content mapping
-## Issues
+## Issues & Roadmap
-I am maintaing a list of issues and features in the [issues](https://github.com/wagtail-examples/wagtail-wordpress-connector/issues) section of the repository.
+Issues and feature requests are tracked in the [GitHub issues](https://github.com/wagtail-examples/wagtail-wordpress-connector/issues) section.
## Contributing
-If you would like to contribute to this project, please fork the repository and submit a pull request.
+Contributions are welcome! To contribute:
+
+1. Fork the repository
+2. Create a feature branch (`git checkout -b feature/amazing-feature`)
+3. Commit your changes (`git commit -m 'Add some amazing feature'`)
+4. Push to the branch (`git push origin feature/amazing-feature`)
+5. Open a Pull Request
+
+## License
+
+This project is licensed under the terms included in the LICENSE file.
diff --git a/commands/anchor_links.py b/commands/anchor_links.py
deleted file mode 100644
index 2f425fd..0000000
--- a/commands/anchor_links.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import requests
-import rich_click as click
-from bs4 import BeautifulSoup as bs
-
-from .inspector import BASE_ENDPOINT, ENDPOINTS
-
-
-@click.command()
-@click.argument("endpoint", required=False)
-def a(endpoint):
- """
- Use this command to inspect the wordpress API.
-
- Specifically look for anchor links in the response of the content field.
- Ths anchor links we are interested in are the ones that are not followed by an image tag
- and link to another page on the same site. e.g.
- Hello world!
- External links are not of interest.
- """
- # If there's no endpoint, show all available endpoints
- if not endpoint:
- for key in ENDPOINTS:
- k = click.style(key, fg="yellow")
- v = click.style(ENDPOINTS[key], fg="green")
- click.echo(f"{k} : {v}")
- help_text = click.style("Use the --help option for more information", fg="red")
- click.echo(help_text)
- return
-
- # If the endpoint is not in the list, show an error message
- if endpoint not in ENDPOINTS:
- click.echo(f"Endpoint {endpoint} not found")
- return
-
- url = f"{BASE_ENDPOINT}{ENDPOINTS[endpoint]}"
- response = requests.get(url)
- response.raise_for_status()
- data = response.json()
- for item in data:
- if "content" in item:
- content = item["content"]["rendered"]
- # if "Hello world!
+ External links are not of interest.
+ """
+ # If the endpoint is not in the list, show an error message
+ if endpoint not in ENDPOINTS:
+ print(f"Endpoint {display_colored_text(endpoint, 'red')} not found")
+ return
+
+ url = f"{BASE_ENDPOINT}{ENDPOINTS[endpoint]}"
+ try:
+ response = requests.get(url)
+ response.raise_for_status()
+ data = response.json()
+
+ found_links = False
+
+ for item in data:
+ if "content" in item:
+ content = item["content"]["rendered"]
+ soup = bs(content, "html.parser")
+
+ for a in soup.find_all("a"):
+ next_el = a.next_element
+ if next_el and not next_el.name == "img":
+ href = a.get("href")
+ if href and href.startswith("http://localhost:8888"):
+ found_links = True
+ title = "no title"
+ if item.get("title"):
+ title = item["title"]
+ if isinstance(title, dict) and "rendered" in title:
+ title = title["rendered"]
+ if item.get("name"):
+ title = item["name"]
+
+ print(
+ display_colored_text(
+ f"Title: {title} ID: {item['id']}", "blue"
+ )
+ )
+ print(a)
+ print()
+
+ if not found_links:
+ print(display_colored_text("No relevant anchor links found", "yellow"))
+
+ except requests.exceptions.RequestException as e:
+ print(display_colored_text(f"Error accessing the API: {e}", "red"))
+
+
+def main():
+ """Main function to handle command line arguments and execute the script."""
+ parser = argparse.ArgumentParser(
+ description="Find anchor links in WordPress API content."
+ )
+
+ parser.add_argument(
+ "endpoint",
+ nargs="?",
+ help="The WordPress API endpoint to inspect. If not provided, will list all available endpoints.",
+ )
+
+ args = parser.parse_args()
+
+ if not args.endpoint:
+ show_endpoints()
+ return
+
+ find_anchor_links(args.endpoint)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/commands/helpers.py b/commands/helpers.py
new file mode 100644
index 0000000..fea1040
--- /dev/null
+++ b/commands/helpers.py
@@ -0,0 +1,48 @@
+"""
+Helper functions and constants for WordPress API inspection commands.
+"""
+
+import os
+from colorama import Fore, Style
+
+# Base endpoint for WordPress API
+BASE_ENDPOINT = os.environ.get("WP_API_ENDPOINT", "http://localhost:8888/wp-json")
+
+# Default number of records per page
+PERPAGE = 100
+
+# Endpoints available in the WordPress API
+ENDPOINTS = {
+ "home": "/",
+ "posts": "/wp/v2/posts",
+ "pages": "/wp/v2/pages",
+ "categories": "/wp/v2/categories",
+ "tags": "/wp/v2/tags",
+ "media": "/wp/v2/media",
+ "users": "/wp/v2/users",
+ "comments": "/wp/v2/comments",
+}
+
+
+def display_colored_text(text, color):
+ """Helper function to display colored text."""
+ colors = {
+ "red": Fore.RED,
+ "green": Fore.GREEN,
+ "yellow": Fore.YELLOW,
+ "blue": Fore.BLUE,
+ }
+ return f"{colors.get(color, '')}{text}{Style.RESET_ALL}"
+
+
+def show_endpoints():
+ """Display all available endpoints."""
+ print("Available endpoints:")
+ for key in ENDPOINTS:
+ k = display_colored_text(key, "yellow")
+ v = display_colored_text(ENDPOINTS[key], "green")
+ print(f"{k} : {v}")
+ help_text = display_colored_text(
+ "Use the --help option for more information", "red"
+ )
+ print(help_text)
diff --git a/commands/inspector.py b/commands/inspector.py
deleted file mode 100644
index 756e34c..0000000
--- a/commands/inspector.py
+++ /dev/null
@@ -1,99 +0,0 @@
-import pprint
-
-import requests
-import rich_click as click
-
-BASE_ENDPOINT = "http://localhost:8888/wp-json"
-ENDPOINTS = {
- "home": "/",
- "posts": "/wp/v2/posts",
- "pages": "/wp/v2/pages",
- "categories": "/wp/v2/categories",
- "tags": "/wp/v2/tags",
- "media": "/wp/v2/media",
- "users": "/wp/v2/users",
- "comments": "/wp/v2/comments",
-}
-PERPAGE = 100
-
-
-@click.command()
-@click.argument("endpoint", required=False)
-@click.option(
- "--all",
- "-a",
- is_flag=True,
- help="Show all records, might need to use the -p option to increase the number of records per page",
-)
-@click.option(
- "--perpage", "-p", default=PERPAGE, help="Request this number of records per page"
-)
-@click.option(
- "--record", "-r", default=None, help="Limit the returned record to it's ID number"
-)
-def i(endpoint, all, perpage, record):
- """
- Use this command to inspect the wordpress API.
-
- You can use the endpoint argument to specify the endpoint you want to inspect.
-
- If you don't specify an endpoint, an index of available endpoints will be shown.
-
- """
- # If there's no endpoint, show all available endpoints
- if not endpoint:
- for key in ENDPOINTS:
- k = click.style(key, fg="yellow")
- v = click.style(ENDPOINTS[key], fg="green")
- click.echo(f"{k} : {v}")
- help_text = click.style("Use the --help option for more information", fg="red")
- click.echo(help_text)
- return
-
- # If the endpoint is not in the list, show an error message
- if endpoint not in ENDPOINTS:
- click.echo(f"Endpoint {endpoint} not found")
- return
-
- if record:
- ENDPOINTS[endpoint] = f"{ENDPOINTS[endpoint]}/{record}"
-
- try:
- if response := requests.get(
- f"{BASE_ENDPOINT}{ENDPOINTS[endpoint]}?per_page={perpage}"
- ):
- if response.status_code != 200:
- click.echo(f"Error: {response.status_code}")
- return
-
- if all:
- data = response.json()
- for i, record in enumerate(data):
- click.echo(click.style(f"Record {i}", fg="blue"))
- for key, value in record.items():
- if isinstance(value, dict):
- click.echo(click.style(f"{key}:", fg="yellow"))
- click.echo(f"{pprint.pformat(value)}")
- else:
- k = click.style(key, fg="yellow")
- v = click.style(pprint.pformat(value), fg="green")
- click.echo(f"{k}: {v}")
- return
-
- if isinstance(data := response.json(), list):
- data = data[0]
- else:
- data = data
-
- for key, value in data.items():
- if isinstance(value, dict):
- click.echo(click.style(f"{key}:", fg="yellow"))
- click.echo(f"{pprint.pformat(value)}")
- else:
- k = click.style(key, fg="yellow")
- v = click.style(pprint.pformat(value), fg="green")
- click.echo(f"{k}: {v}")
-
- except requests.exceptions.ConnectionError:
- click.echo("Error: Could not connect to the API")
- return
diff --git a/commands/wp_api_inspector.py b/commands/wp_api_inspector.py
new file mode 100644
index 0000000..d3014b7
--- /dev/null
+++ b/commands/wp_api_inspector.py
@@ -0,0 +1,107 @@
+import argparse
+import pprint
+
+import requests
+from colorama import init
+from commands.helpers import (
+ BASE_ENDPOINT,
+ ENDPOINTS,
+ PERPAGE,
+ display_colored_text,
+ show_endpoints,
+)
+
+# Initialize colorama
+init()
+
+
+def inspect_endpoint(endpoint, all_records=False, perpage=PERPAGE, record=None):
+ """Inspect a specific WordPress API endpoint."""
+ # If the endpoint is not in the list, show an error message
+ if endpoint not in ENDPOINTS:
+ print(f"Endpoint {endpoint} not found")
+ return
+
+ endpoint_url = ENDPOINTS[endpoint]
+ if record:
+ endpoint_url = f"{endpoint_url}/{record}"
+
+ try:
+ response = requests.get(f"{BASE_ENDPOINT}{endpoint_url}?per_page={perpage}")
+
+ if response.status_code != 200:
+ print(f"Error: {response.status_code}")
+ return
+
+ data = response.json()
+
+ if all_records and isinstance(data, list):
+ for i, record in enumerate(data):
+ print(display_colored_text(f"Record {i}", "blue"))
+ display_record(record)
+ return
+
+ # If not showing all records, display the first one or the single object
+ if isinstance(data, list):
+ data = data[0]
+
+ display_record(data)
+
+ except requests.exceptions.ConnectionError:
+ print("Error: Could not connect to the API")
+
+
+def display_record(record):
+ """Display a record with formatted output."""
+ for key, value in record.items():
+ if isinstance(value, dict):
+ print(display_colored_text(f"{key}:", "yellow"))
+ print(f"{pprint.pformat(value)}")
+ else:
+ k = display_colored_text(key, "yellow")
+ v = display_colored_text(pprint.pformat(value), "green")
+ print(f"{k}: {v}")
+
+
+def main():
+ """Main function to handle command line arguments and execute the inspector."""
+ parser = argparse.ArgumentParser(
+ description="Use this command to inspect the WordPress API."
+ )
+
+ parser.add_argument(
+ "endpoint",
+ nargs="?",
+ help="Specify the endpoint you want to inspect. If not provided, an index of available endpoints will be shown.",
+ )
+
+ parser.add_argument(
+ "--all",
+ "-a",
+ action="store_true",
+ help="Show all records, might need to use the -p option to increase the number of records per page",
+ )
+
+ parser.add_argument(
+ "--perpage",
+ "-p",
+ type=int,
+ default=PERPAGE,
+ help=f"Request this number of records per page (default: {PERPAGE})",
+ )
+
+ parser.add_argument(
+ "--record", "-r", help="Limit the returned record to its ID number"
+ )
+
+ args = parser.parse_args()
+
+ if not args.endpoint:
+ show_endpoints()
+ return
+
+ inspect_endpoint(args.endpoint, args.all, args.perpage, args.record)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/field_mapping.md b/docs/field_mapping.md
new file mode 100644
index 0000000..34c5734
--- /dev/null
+++ b/docs/field_mapping.md
@@ -0,0 +1,125 @@
+# Field Mapping Guide
+
+This guide explains how to create custom field mappings between WordPress and Wagtail models.
+
+## Basic Field Mapping
+
+The `FIELD_MAPPING` attribute in WordPress models is used to map WordPress fields to Wagtail fields:
+
+```python
+FIELD_MAPPING = {
+ "wp_field_name": "wagtail_field_name",
+ "title": "title",
+ "content": "body",
+ # etc.
+}
+```
+
+## StreamField Mapping
+
+For WordPress content that should be converted to Wagtail StreamFields, you need to define a `get_streamfield_mapping` method:
+
+```python
+def get_streamfield_mapping(self):
+ return {
+ "content": "body", # Map WP content to a Wagtail StreamField named "body"
+ "excerpt": "intro", # Map WP excerpt to a StreamField named "intro"
+ }
+```
+
+## Custom Field Processing
+
+The connector provides field processors for handling content transformations. You can customize these for your specific needs:
+
+### RichText Fields
+
+For processing RichText fields and converting links:
+
+```python
+from wp_connector.richtext_field_processor import FieldProcessor
+
+# Use the field processor after transfer
+field_processor = FieldProcessor(wordpress_instance)
+field_processor.process_fields()
+```
+
+### StreamField Data
+
+For converting HTML content to StreamField blocks:
+
+```python
+from wp_connector.streamfieldable import StreamFieldable
+
+stream_data = StreamFieldable(
+ obj=wordpress_object,
+ content=wordpress_object.content
+)
+wagtail_page.body = stream_data.streamdata
+```
+
+## Common Field Mapping Patterns
+
+### Posts to Blog Pages
+
+```python
+class WPPost(WordpressModel, ExportableMixin):
+ SOURCE_URL = "/wp-json/wp/v2/posts"
+ WAGTAIL_PAGE_MODEL = "blog.BlogPage"
+ WAGTAIL_PAGE_MODEL_PARENT = "blog.BlogIndexPage"
+ FIELD_MAPPING = {
+ "title": "title",
+ "content": "body",
+ "excerpt": "intro",
+ "date": "date",
+ "slug": "slug",
+ }
+```
+
+### Pages to Standard Pages
+
+```python
+class WPPage(WordpressModel, ExportableMixin):
+ SOURCE_URL = "/wp-json/wp/v2/pages"
+ WAGTAIL_PAGE_MODEL = "standardpages.StandardPage"
+ WAGTAIL_PAGE_MODEL_PARENT = "home.HomePage"
+ FIELD_MAPPING = {
+ "title": "title",
+ "content": "body",
+ "slug": "slug",
+ }
+```
+
+### Media to Images/Documents
+
+```python
+class WPMedia(WordpressModel):
+ SOURCE_URL = "/wp-json/wp/v2/media"
+ # Media handling usually requires custom processing
+ # to download files and create Wagtail images/documents
+```
+
+## Advanced Configuration
+
+### Handling Custom Fields from WordPress
+
+If your WordPress instance has custom fields (from plugins like Advanced Custom Fields):
+
+1. Ensure the field is exposed in the WordPress REST API
+2. Add the field to your WordPress model
+3. Include it in your field mapping to the corresponding Wagtail field
+
+### Mapping to Wagtail Snippets
+
+For WordPress data that should become Wagtail snippets:
+
+```python
+# When transferring content that relates to this data
+def set_custom_relation(self, wagtail_page):
+ if custom_data := self.obj.custom_data:
+ # Get or create a Wagtail snippet
+ custom_snippet, created = CustomSnippet.objects.get_or_create(
+ name=custom_data.name,
+ )
+ # Set the relationship
+ wagtail_page.custom_field = custom_snippet
+```
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..9ca5d2a
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,56 @@
+# WordPress to Wagtail Connector Documentation
+
+Welcome to the documentation for the WordPress to Wagtail Connector. This project helps you migrate content from WordPress to Wagtail.
+
+## Documentation Index
+
+- [Setup & Usage Guide](./setup.md) - Main guide for setting up and using the connector
+- [Field Mapping Guide](./field_mapping.md) - Detailed information on mapping WordPress fields to Wagtail
+- [Troubleshooting Guide](./troubleshooting.md) - Solutions for common issues and command reference
+- [Project Structure Guide](./project_structure.md) - Overview of the project organization and components
+
+## Quick Command Reference
+
+The project uses Make commands for its operations:
+
+```bash
+# Get help with all available commands
+make help
+
+# Start everything (WordPress, import data, Wagtail)
+make start
+
+# Stop services
+make stop
+```
+
+See the [Troubleshooting Guide](./troubleshooting.md) for a complete command reference.
+
+## Screenshots
+
+### Wagtail Site with Imported WordPress Data
+
+
+
+### Wagtail Admin for Managing Imported Data
+
+
+
+### Django Admin for Transferring Data
+
+
+
+## Additional Resources
+
+- [GitHub Repository](https://github.com/wagtail-examples/wagtail-wordpress-connector)
+- [Wagtail Documentation](https://docs.wagtail.org/)
+- [WordPress REST API Documentation](https://developer.wordpress.org/rest-api/)
+- [Changelog](../CHANGELOG.md)
+
+## Project Status
+
+This is an experimental project to import WordPress content including pages and posts into Wagtail. It's not yet ready for production use, but most of the core functionality is in place.
+
+## Contributing
+
+Contributions to the WordPress to Wagtail connector are welcome! See the [README.md](../README.md) file for details on how to contribute.
diff --git a/docs/project_structure.md b/docs/project_structure.md
new file mode 100644
index 0000000..2d66187
--- /dev/null
+++ b/docs/project_structure.md
@@ -0,0 +1,85 @@
+# Project Structure Guide
+
+This document provides an overview of the WordPress to Wagtail Connector project structure and its key components.
+
+## Directory Structure
+
+```
+wagtail-wordpress-connector/
+├── app/ # Wagtail application code
+│ ├── blog/ # Blog page models
+│ ├── home/ # Home page models
+│ ├── search/ # Search functionality
+│ ├── settings/ # Django settings
+│ ├── static_compiled/ # Compiled static files
+│ ├── static_src/ # Source static files
+│ ├── style_guide/ # Style guide app
+│ └── templates/ # Global templates
+├── commands/ # CLI commands and utilities
+│ ├── find_anchor_links.py # Tool to identify anchor links
+│ └── wp_api_inspector.py # Tool to inspect WordPress API
+├── docs/ # Documentation files
+├── scripts/ # JavaScript build scripts
+├── wordpress.docker/ # WordPress Docker setup
+├── wordpress.testdata/ # Test data for WordPress
+└── wp_connector/ # WordPress connector app
+ ├── management/ # Django management commands
+ ├── migrations/ # Database migrations
+ ├── models/ # Django models for WordPress data
+ ├── templates/ # Connector templates
+ └── tests/ # Test suite
+```
+
+## Key Components
+
+### wp_connector
+
+The core of the connector is the `wp_connector` app which contains:
+
+- **Models** (`models/`): Django models that represent WordPress content types
+- **Importer** (`importer.py`): Handles importing data from WordPress API
+- **Exporter** (`exporter.py`): Handles exporting WordPress data to Wagtail
+- **Admin interface** (`admin.py`): Custom Django admin interface for managing imports
+- **RichText Field Processor** (`richtext_field_processor.py`): Handles content conversion
+- **StreamField Converter** (`streamfieldable.py`): Converts WordPress HTML to Wagtail StreamFields
+
+### Commands
+
+The `commands` directory contains utility scripts:
+
+- **wp_api_inspector.py**: Tool to inspect WordPress API endpoints
+- **find_anchor_links.py**: Tool to identify anchor links in WordPress content
+
+### Wagtail App
+
+The `app` directory contains the Wagtail application:
+
+- **blog**: Blog page models that WordPress posts are mapped to
+- **home**: Home page models that WordPress pages can be mapped to
+
+## Flow of Data
+
+The WordPress to Wagtail migration follows this general flow:
+
+1. **Import**: Data is imported from WordPress API into Django models
+ - WordPress content is saved in Django models in the `wp_connector` app
+ - Foreign keys and relationships between content are preserved
+
+2. **Transfer**: Data is transferred from Django models to Wagtail
+ - WordPress content is mapped to Wagtail pages and snippets
+ - Content is transformed as needed (HTML to StreamFields, etc.)
+ - Relationships are re-established in Wagtail
+
+3. **Post-processing**: Additional tasks are performed
+ - Redirects are created from WordPress URLs to Wagtail URLs
+ - Anchor links are converted to Wagtail internal links
+ - Media files are processed and added to Wagtail media library
+
+## Custom Extension Points
+
+The connector is designed to be extended in several ways:
+
+- **Model Mapping**: Define how WordPress models map to Wagtail models
+- **Field Mapping**: Define how WordPress fields map to Wagtail fields
+- **Content Processing**: Customize how content is processed during transfer
+- **Streamfield Mapping**: Define how WordPress content is converted to StreamFields
diff --git a/docs/setup.md b/docs/setup.md
index 87c358c..71b034b 100644
--- a/docs/setup.md
+++ b/docs/setup.md
@@ -2,78 +2,105 @@
You can run this example as a test site for your own WordPress to Wagtail migration.
-This example has a Wordpress instance with test data and a Wagtail instance with the WordPress connector installed so you can see how the importer works.
+This example has a WordPress instance with test data and a Wagtail instance with the WordPress connector installed so you can see how the importer works.
+
+## Workflow Overview
+
+The migration process follows these steps:
+
+1. Set up the WordPress instance with test data
+2. Set up the Wagtail instance
+3. Import WordPress data into Django models
+4. Inspect and manage the imported content using the Django admin
+5. Transfer selected content to Wagtail using the admin actions
+6. Manage the transferred content in Wagtail
+7. Create redirects and update anchor links as needed
+
+## Requirements
+
+- Python 3.13+
+- UV for dependency management
+- Docker for WordPress
+- Wagtail 7.0+
+- Django 5.2+
## The CLI
-The CLI is used to run the example. The CLI is a wrapper around all the parts required to run the example. It uses Docker to run the WordPress instance and UV to run the Wagtail/Django instance.
+The CLI is used to run the example. The CLI is a wrapper around all the parts required to run the example. It uses Docker to run the WordPress instance and Make commands to run the Wagtail/Django instance.
-Once you have followed the virtual environment setup instructions below, you can run `go` to see the available commands.
+Once you have followed the virtual environment setup instructions below, you can run `make help` to see the available commands.
-## Wordpress CLI and test data
+## WordPress CLI and test data
-The WordPress CLI is used to setup and initialse the WordPress instance.
+The WordPress CLI is used to setup and initialize the WordPress instance.
-### Create a virtual environment and install the requirements then activate the virtual environment:
+### Create a virtual environment and install the requirements
-```
+```bash
+# Create a virtual environment using UV
uv venv
+
+# Activate the virtual environment (Linux/macOS)
+source .venv/bin/activate
+
+# Activate the virtual environment (Windows)
+.venv\Scripts\activate
```
-### Start up the wordress instance and load the test data:
+### Start up the WordPress instance and load the test data
-Docker is used to run an example WordPress instance with a theme and test data installed. This test data is available for [Wordpress Theme Design](https://raw.githubusercontent.com/WPTT/theme-unit-test/master/themeunittestdata.wordpress.xml) and has a lot of content and layouts that may not be appropriate for all use cases for transferring data accross to Wagtail but it's a good test bed to get started.
+Docker is used to run an example WordPress instance with a theme and test data installed. This test data is available for [WordPress Theme Design](https://raw.githubusercontent.com/WPTT/theme-unit-test/master/themeunittestdata.wordpress.xml) and has a lot of content and layouts that may not be appropriate for all use cases for transferring data across to Wagtail but it's a good test bed to get started.
-The example has it's JSON api enabled so the importer can access the data.
+The example has its JSON API enabled so the importer can access the data.
-#### Build and initialises the wordpress instance
+#### Build and initializes the WordPress instance
-```
-uv run wp build
+```bash
+make wp-build
```
-#### Start the wordpress docker container
+#### Start the WordPress docker container
-```
-uv run wp up
+```bash
+make wp-up
```
#### Load the test data
-```
-uv run wp load
+```bash
+make wp-load
```
You can access the WordPress site at `http://localhost:8888` with test data loaded.
-You can login to the Wordpress admin is at `http://localhost:8888/wp-admin` with the username `admin` and password `password`.
+You can login to the WordPress admin at `http://localhost:8888/wp-admin` with the username `admin` and password `password`.
## Wagtail and Django
-Wagtail and Django are not run in Docker but are run in a virtual environment using Poetry.
+Wagtail and Django are not run in Docker but are run in a virtual environment using UV.
-### Initilase and start Wagtail and Django:
+### Initialize and start Wagtail and Django
-```
-uv run wt migrate
-uv run wt superuser
-uv run wt run
+```bash
+make wt-migrate
+make wt-superuser
+make wt-run
```
You can access the Wagtail site at `http://localhost:8000` with the Wagtail admin at `http://localhost:8000/admin`
-The username and passowrd you added above can be used to log into the Wagtail admin.
+The username and password you added above can be used to log into the Wagtail admin.
At this point there is no data in the Wagtail instance. You should see the Wagtail welcome page.
**Important** Go to the Wagtail admin and create a page, under the Home Page, called `Blog` and publish it. This is the parent page for all blog pages and is required by the transfer process. *You can name the page anything you like.*
-### Importing the data from Wordpress into Django
+### Importing the data from WordPress into Django
-The importer is a sequence of django management commands. To run the importer and import all the data from the wordpress instance, run:
+The importer is a sequence of Django management commands. To run the importer and import all the data from the WordPress instance, run:
-```
-uv run dj all
+```bash
+make import-all
```
This will import the whole sample data set into the Django instance.
@@ -88,11 +115,11 @@ The dataset includes:
- Pages
- Tags
-You can browse the django admin site to inspect the imported content.
+You can browse the Django admin site to inspect the imported content.
-The setup is now complete and ready for the wordpress content to be transfered to Wagtail. This is done using django-admin actions.
+The setup is now complete and ready for the WordPress content to be transferred to Wagtail. This is done using Django-admin actions.
-The django admin for transferring data is at `http://localhost:8000/import-admin`
+The Django admin for transferring data is at `http://localhost:8000/import-admin`
## Transferring data to Wagtail
@@ -103,27 +130,27 @@ Transferring data to Wagtail is done using the Django admin. You can transfer po
### Transferring Posts
-Posts will need a parent page to be transferred to. First create a page in the Wagtail admin using the BlogInxexPage type. This will be the parent page for all the blog posts.
+Posts will need a parent page to be transferred to. First create a page in the Wagtail admin using the BlogIndexPage type. This will be the parent page for all the blog posts.
From this page
-1. Select the posts you want to tansfer (you can select all by clicking the checkbox in the header)
+1. Select the posts you want to transfer (you can select all by clicking the checkbox in the header)
2. Select the action `Create new Wagtail Pages from selected`
3. Click `Go`
-4. The posts will be tansferred to Wagtail as blog pages
+4. The posts will be transferred to Wagtail as blog pages
*The list display is limited to 100 items at a time so you may need to use the `Select all` link next to the Go button to select all the posts.*
### Transferring Pages
-1. Select the pages you want to export (you can select all by clicking the checkbox in the header)
+1. Select the pages you want to transfer (you can select all by clicking the checkbox in the header)
2. Select the action `Create new Wagtail Pages from selected`
3. Click `Go`
-4. The pages will be exported to Wagtail as pages
+4. The pages will be transferred to Wagtail as pages
#### Authors, Categories and Tags
-If a wordpress page has foriegn keys to data such as authors, categories or tags, the transfer process will create [Wagtail Snippets](https://docs.wagtail.org/en/stable/topics/snippets/index.html) and [taggit tags](https://docs.wagtail.org/en/stable/reference/pages/model_recipes.html#managing-tags-as-snippets) to hold the data and add the appropriate relationships to the Wagtail pages.
+If a WordPress page has foreign keys to data such as authors, categories or tags, the transfer process will create [Wagtail Snippets](https://docs.wagtail.org/en/stable/topics/snippets/index.html) and [taggit tags](https://docs.wagtail.org/en/stable/reference/pages/model_recipes.html#managing-tags-as-snippets) to hold the data and add the appropriate relationships to the Wagtail pages.
### Further actions
@@ -140,3 +167,184 @@ You can create the redirects using the `Create Wagtail Redirects from selected`
Richtext fields in Wagtail do not support regular anchor links. To handle this you can use the action `Update Anchor Links in content fields` to convert the anchor links to Wagtail internal links.
This works for both single richtext fields and richtext fields within StreamFields.
+
+## Advanced Admin Actions
+
+The import admin interface provides additional actions that can be useful during the WordPress to Wagtail migration process:
+
+### Updating Existing Wagtail Pages
+
+If you've already transferred content to Wagtail but need to update it with changes from WordPress:
+
+1. Select the WordPress content that has already been transferred to Wagtail
+2. Choose the action `Update Existing Wagtail Pages`
+3. Click `Go`
+
+This will update the corresponding Wagtail pages with any changes from the WordPress content while preserving the Wagtail page IDs.
+
+### Deleting Wagtail Pages
+
+If you need to remove Wagtail pages that were created from WordPress content:
+
+1. Select the WordPress content whose Wagtail pages you want to delete
+2. Choose the action `Delete Existing Wagtail Pages from selected`
+3. Click `Go`
+
+This will delete the Wagtail pages but keep the WordPress content in Django, allowing you to transfer it again if needed.
+
+### Deleting WordPress Records
+
+To remove WordPress content from the Django database:
+
+1. Select the WordPress content you want to delete
+2. Choose the action `Delete WordPress Records from selected`
+3. Click `Go`
+
+Note that this action does not delete any corresponding Wagtail pages that might have been created.
+
+## Media Handling
+
+The connector includes support for media files:
+
+- WordPress media items are imported into Django models
+- When transferring content to Wagtail, media references can be processed
+- Support for featured images and inline images is included
+- Note: Media handling is a complex area, and you may need to customize the implementation for your specific needs
+
+## Completion
+
+Once you've transferred all your content to Wagtail, you can remove the WordPress connector module. Your Wagtail site will have no dependencies on the WordPress instance, allowing you to manage it like any other Wagtail site.
+
+## Convenience Commands
+
+The project includes several convenience commands to simplify common tasks:
+
+```bash
+# Set up and start everything in one command
+make start
+
+# Stop all running services
+make stop
+
+# Destroy and cleanup WordPress and Wagtail
+make destroy
+```
+
+Using `make start` will execute the following commands in sequence:
+
+- WordPress initial setup (wp-build)
+- Start WordPress container (wp-up)
+- Import WordPress demo data (wp-load)
+- Run Wagtail migrations (wt-migrate)
+- Create a Wagtail superuser (wt-superuser)
+- Import all WordPress data (import-all)
+- Start the Wagtail development server (wt-run)
+
+## Frontend Asset Management
+
+The project includes several commands for managing frontend assets:
+
+```bash
+# Install Node.js dependencies
+make node-setup
+
+# Build all frontend assets for production
+make node-build
+
+# Start the frontend development server
+make node-start
+
+# Compile CSS styles
+make node-styles
+
+# Watch and compile CSS styles
+make node-styles-watch
+
+# Compile JavaScript
+make node-scripts
+
+# Watch and compile JavaScript
+make node-scripts-watch
+```
+
+These commands help you manage the frontend assets when customizing the appearance and behavior of your Wagtail site after the WordPress content has been imported.
+
+## Additional Tools
+
+The connector includes several utility tools to help with the migration process:
+
+- `wp_api_inspector.py` - Helps inspect WordPress API endpoints
+- `find_anchor_links.py` - Identifies anchor links in WordPress content for conversion
+- Field processors for handling richtext content and StreamFields
+- Admin views and actions for managing the transfer process
+
+## Customizing the Connector
+
+The connector is designed to be extensible. You may need to customize it for your specific WordPress setup and Wagtail models.
+
+### Model Mapping
+
+Each WordPress model (posts, pages, etc.) can be mapped to a corresponding Wagtail model through configuration attributes:
+
+```python
+class WPPost(WordpressModel, ExportableMixin):
+ """Model definition for Posts."""
+
+ SOURCE_URL = "/wp-json/wp/v2/posts"
+ WAGTAIL_PAGE_MODEL = "blog.BlogPage"
+ WAGTAIL_PAGE_MODEL_PARENT = "blog.BlogIndexPage"
+ FIELD_MAPPING = {
+ "title": "title",
+ "content": "body",
+ "excerpt": "intro",
+ "date": "date",
+ }
+```
+
+For detailed information on field mapping between WordPress and Wagtail, see the [Field Mapping Guide](./field_mapping.md).
+
+### StreamField Mapping
+
+For WordPress content that should be converted to Wagtail StreamFields:
+
+```python
+def get_streamfield_mapping(self):
+ return {
+ "content": "body",
+ "excerpt": "intro",
+ }
+```
+
+### Custom Field Processing
+
+You can extend the `FieldProcessor` class to handle custom content conversion needs:
+
+```python
+from wp_connector.richtext_field_processor import FieldProcessor
+
+class CustomFieldProcessor(FieldProcessor):
+ def process_fields(self):
+ # Custom processing logic here
+ super().process_fields()
+```
+
+### Adding Support for Additional WordPress Content Types
+
+To add support for additional WordPress content types:
+
+1. Create a new model class extending `WordpressModel`
+2. Define the required attributes (SOURCE_URL, etc.)
+3. Register the model with the admin interface
+4. Create or modify the corresponding Wagtail model
+
+## Troubleshooting
+
+For help with common issues and a complete command reference, see the [Troubleshooting Guide](./troubleshooting.md).
+
+## Contributing
+
+Contributions to the WordPress to Wagtail connector are welcome! See the [README.md](../README.md) file for details on how to contribute.
+
+## License
+
+This project is licensed under the terms included in the LICENSE file.
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
new file mode 100644
index 0000000..826ec9c
--- /dev/null
+++ b/docs/troubleshooting.md
@@ -0,0 +1,81 @@
+# Troubleshooting Guide
+
+## Common Issues
+
+### API Connection Problems
+
+If you're having trouble connecting to the WordPress API:
+
+1. Ensure your WordPress instance has the REST API enabled
+2. Check that the API endpoint URLs are correct in your model definitions
+3. Verify that authentication is configured properly if your API requires it
+
+### Page Hierarchy Issues
+
+If pages aren't appearing in the correct hierarchy:
+
+1. Run `make wt-fixtree` to repair the Wagtail page tree
+2. Ensure parent pages exist before transferring child pages
+3. Check that parent-child relationships in WordPress are correctly mapped
+
+### Missing Content After Transfer
+
+If content appears to be missing after transfer:
+
+1. Check the field mappings in your WordPress models
+2. Verify that StreamField mappings are correctly configured
+3. Look for any errors in the Django admin messages
+
+### Anchor Link Conversion Issues
+
+If anchor links aren't converting properly:
+
+1. Use `find_anchor_links.py` to identify all anchor links in your content
+2. Ensure all referenced pages have been transferred to Wagtail first
+3. Run the `Update Anchor Links in content fields` action after all pages are transferred
+
+## CLI Command Reference
+
+The project includes several CLI commands to facilitate the migration process:
+
+### WordPress Commands
+
+- `make wp-build` - WordPress: initial setup
+- `make wp-up` - WordPress: start the container
+- `make wp-load` - WordPress: import the demo data
+- `make wp-down` - WordPress: stop the container
+- `make wp-destroy` - WordPress: destroy the container
+
+### Wagtail Commands
+
+- `make wt-migrate` - Wagtail: run migrations
+- `make wt-superuser` - Wagtail: create superuser
+- `make wt-run` - Wagtail: run the server
+- `make wt-fixtree` - Wagtail: fix the tree
+
+### Import Commands
+
+- `make import-all` - Django: import all data from WordPress
+- `make import-authors` - Django: import authors from WordPress
+- `make import-categories` - Django: import categories from WordPress
+- `make import-tags` - Django: import tags from WordPress
+- `make import-pages` - Django: import pages from WordPress
+- `make import-posts` - Django: import posts from WordPress
+- `make import-media` - Django: import media from WordPress
+- `make import-comments` - Django: import comments from WordPress
+
+### Convenience Commands
+
+- `make start` - Run all commands to set up and start the development environment
+- `make stop` - Stop all running services
+- `make destroy` - Destroy and cleanup WordPress and Wagtail
+
+### Node.js Commands
+
+- `make node-setup` - Install Node.js dependencies
+- `make node-build` - Build all frontend assets for production
+- `make node-start` - Start the frontend development server
+- `make node-styles` - Compile CSS styles
+- `make node-styles-watch` - Watch and compile CSS styles
+- `make node-scripts` - Compile JavaScript
+- `make node-scripts-watch` - Watch and compile JavaScript
diff --git a/pyproject.toml b/pyproject.toml
index 37c1f34..efaed50 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,19 +3,19 @@ name = "wagtail-wordpress-connector"
version = "0.1.0"
description = "This is an experimental project to import WordPress content including pages and posts into Wagtail."
readme = "README.md"
-requires-python = ">=3.12"
+requires-python = ">=3.13"
dependencies = [
"beautifulsoup4>=4.13.3",
"django>=5.2",
"django-extensions>=3.2.3",
"jmespath>=1.0.1",
"requests>=2.32.3",
- "rich-click>=1.8.6",
"wagtail>=7.0",
]
[dependency-groups]
dev = [
+ "colorama>=0.4.6",
"coverage>=7.6.12",
"pre-commit>=4.1.0",
"responses>=0.25.6",
@@ -28,12 +28,3 @@ build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["."]
-
-[project.scripts]
-go = "commands.cli:go"
-wp = "commands.cli:wp"
-wt = "commands.cli:wt"
-dj = "commands.cli:dj"
-
-i = "commands.inspector:i"
-a = "commands.anchor_links:a"
diff --git a/uv.lock b/uv.lock
index ed52d41..2610193 100644
--- a/uv.lock
+++ b/uv.lock
@@ -86,18 +86,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
]
-[[package]]
-name = "click"
-version = "8.1.8"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
-]
-
[[package]]
name = "colorama"
version = "0.4.6"
@@ -364,27 +352,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/60/fe/31f76f5cb2579bdda208aa257ce5482653f22ab1bad3e128fe2f803fa2f1/laces-0.1.2-py3-none-any.whl", hash = "sha256:980cdaf9a31e883a2b8198132e2388931a4eb8814f5bfa5d8bba13ff9f657b7c", size = 22462 },
]
-[[package]]
-name = "markdown-it-py"
-version = "3.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "mdurl" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
-]
-
-[[package]]
-name = "mdurl"
-version = "0.1.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
-]
-
[[package]]
name = "nodeenv"
version = "1.9.1"
@@ -494,15 +461,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/43/b3/df14c580d82b9627d173ceea305ba898dca135feb360b6d84019d0803d3b/pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b", size = 220560 },
]
-[[package]]
-name = "pygments"
-version = "2.19.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
-]
-
[[package]]
name = "pyyaml"
version = "6.0.2"
@@ -558,33 +516,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/92/c4/8d23584b3a3471ea6f5a18cfb035e11eeb9fa9b3112d901477c6ad10cc4e/responses-0.25.6-py3-none-any.whl", hash = "sha256:9cac8f21e1193bb150ec557875377e41ed56248aed94e4567ed644db564bacf1", size = 34730 },
]
-[[package]]
-name = "rich"
-version = "13.9.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "markdown-it-py" },
- { name = "pygments" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
-]
-
-[[package]]
-name = "rich-click"
-version = "1.8.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "rich" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ea/e3/ff1c715b673ec9e01f4482d8d0edfd9adf891f3630d83e695b38337a3889/rich_click-1.8.6.tar.gz", hash = "sha256:8a2448fd80e3d4e16fcb3815bfbc19be9bae75c9bb6aedf637901e45f3555752", size = 38247 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/09/c20b04b6c9cf273995753f226ca51656e00f8a37f1e723f8c713b93b2ad4/rich_click-1.8.6-py3-none-any.whl", hash = "sha256:55fb571bad7d3d69ac43ca45f05b44616fd019616161b1815ff053567b9a8e22", size = 35076 },
-]
-
[[package]]
name = "ruff"
version = "0.11.10"
@@ -716,12 +647,12 @@ dependencies = [
{ name = "django-extensions" },
{ name = "jmespath" },
{ name = "requests" },
- { name = "rich-click" },
{ name = "wagtail" },
]
[package.dev-dependencies]
dev = [
+ { name = "colorama" },
{ name = "coverage" },
{ name = "pre-commit" },
{ name = "responses" },
@@ -735,12 +666,12 @@ requires-dist = [
{ name = "django-extensions", specifier = ">=3.2.3" },
{ name = "jmespath", specifier = ">=1.0.1" },
{ name = "requests", specifier = ">=2.32.3" },
- { name = "rich-click", specifier = ">=1.8.6" },
{ name = "wagtail", specifier = ">=7.0" },
]
[package.metadata.requires-dev]
dev = [
+ { name = "colorama", specifier = ">=0.4.6" },
{ name = "coverage", specifier = ">=7.6.12" },
{ name = "pre-commit", specifier = ">=4.1.0" },
{ name = "responses", specifier = ">=0.25.6" },