Repository: wagtail/wagtail

Issue:
Add ability for slugs to be auto incremented on a per model basis
### Is your proposal related to a problem?

I would like to be able to use auto incrementing page slugs in the case of a duplicate slug within the current parent page's URL path.

Example use case: An event calendar where I don't really care so much about the slugs, but there is a high potential for duplicates with the types of events that may be submitted.

The ability to generate 'nice' URLs is a positive selling point of Wagtail that we don't want to undermine but we do want to help users create pages easily.

### Describe the solution you'd like

`Page` models can have an `auto_increment_slug` meta flag, instead of a global Django setting.

When the value is `True`, and pages are created as children of that page (important: the slug behaviour will be determined by parent models, not the page models itself) will have an extra step on form creation.

If a page is being created (not edited) and there is a value error because of a duplicate URL path, instead of failing the form submission it will automatically append an incremented number to the end of the URL. This incrementing number would need to parse the conflicting page path for the last number and then add to it.

**Edge cases to consider**

* What if the path part is not really meant to be an incremental number (E.g. `path/to/blog-post-about-pi-being-3`)
* Confirmation that we want the flag on parent pages and not the page itself that is being created
* Supporting simple numbers only?
* What if the number is `0`, should we increment from `1`
* Should we provide the ability to set the start number `auto_increment_slug = 0` or `auto_increment_slug = 10` or just always assume it is 1 and the first page that contains the slug without any increment will be the zeroth item (without the `-0`)


**Requirements**

* Unit tests are a must
* Documentation for this feature to be added - maybe in https://docs.wagtail.org/en/latest/advanced_topics/customisation/page_editing_interface.html#customising-generated-forms & also page model reference


### Describe alternatives you've considered

* Do not try to solve this in an automated way and keep the page validation as is, users can decide on what the slug should be manually.

### Additional context

* An older PR has an approach (settings) to this problem - see https://github.com/wagtail/wagtail/pull/2454

> This proposed behaviour has come up again independently in #3477. We've now discussed this among the core team - while auto-rewriting of slugs is a behaviour we'd want to avoid in the default case (because the ability to generate 'nice' URLs is a positive selling point of Wagtail that we don't want to undermine...) we're agreed that this would be a worthwhile feature to support on a per-model basis (perhaps with a `auto_increment_slug` meta flag).
> _Originally posted by @gasman in https://github.com/wagtail/wagtail/issues/2454#issuecomment-290208940_ (March 2017)
            

Add ability for slugs to be auto incremented on a per model basis
### Is your proposal related to a problem?

I would like to be able to use auto incrementing page slugs in the case of a duplicate slug within the current parent page's URL path.

Example use case: An event calendar where I don't really care so much about the slugs, but there is a high potential for duplicates with the types of events that may be submitted.

The ability to generate 'nice' URLs is a positive selling point of Wagtail that we don't want to undermine but we do want to help users create pages easily.

### Describe the solution you'd like

`Page` models can have an `auto_increment_slug` meta flag, instead of a global Django setting.

When the value is `True`, and pages are created as children of that page (important: the slug behaviour will be determined by parent models, not the page models itself) will have an extra step on form creation.

If a page is being created (not edited) and there is a value error because of a duplicate URL path, instead of failing the form submission it will automatically append an incremented number to the end of the URL. This incrementing number would need to parse the conflicting page path for the last number and then add to it.

**Edge cases to consider**

* What if the path part is not really meant to be an incremental number (E.g. `path/to/blog-post-about-pi-being-3`)
* Confirmation that we want the flag on parent pages and not the page itself that is being created
* Supporting simple numbers only?
* What if the number is `0`, should we increment from `1`
* Should we provide the ability to set the start number `auto_increment_slug = 0` or `auto_increment_slug = 10` or just always assume it is 1 and the first page that contains the slug without any increment will be the zeroth item (without the `-0`)


**Requirements**

* Unit tests are a must
* Documentation for this feature to be added - maybe in https://docs.wagtail.org/en/latest/advanced_topics/customisation/page_editing_interface.html#customising-generated-forms & also page model reference


### Describe alternatives you've considered

* Do not try to solve this in an automated way and keep the page validation as is, users can decide on what the slug should be manually.

### Additional context

* An older PR has an approach (settings) to this problem - see https://github.com/wagtail/wagtail/pull/2454

> This proposed behaviour has come up again independently in #3477. We've now discussed this among the core team - while auto-rewriting of slugs is a behaviour we'd want to avoid in the default case (because the ability to generate 'nice' URLs is a positive selling point of Wagtail that we don't want to undermine...) we're agreed that this would be a worthwhile feature to support on a per-model basis (perhaps with a `auto_increment_slug` meta flag).
> _Originally posted by @gasman in https://github.com/wagtail/wagtail/issues/2454#issuecomment-290208940_ (March 2017)
            



Discussion:
@lb-  I want to give it a try, 
@suyash5053  may I suggest you keep trying to get to the bottom of https://github.com/wagtail/wagtail/pull/10122 and see if you can finish that off before tacking this issue (which will likely involve a lot more complexity and unit testing scenarios).

You are free to help with whatever you want, but it's often better to try to focus on one thing at a time.
Your proposal to add an `auto_increment_slug` meta flag to page models, which would allow for automatic generation of incremented slugs in cases of duplicate URLs, is a reasonable solution to the problem you've described.

To address the edge cases you've mentioned, it would be necessary to have additional logic to determine whether the conflicting path part is actually an incremental number. This could be done using regular expressions or other pattern matching techniques.

It's worth noting that this feature would only be useful for certain types of page models where slug uniqueness is not critical, and where having a duplicate slug with an incremented number would still be acceptable. Therefore, it would be important to make it clear in the documentation that this is an optional feature that should only be used in appropriate situations.

Please assign me this issue, I am Interested.
@AnuragMishraa please try to add constructive comments, I've hidden your message for now as it basically repeats the issue. 

Once you feel confident with contributing and have followed the steps on https://github.com/wagtail/wagtail/issues/10070 you will hopefully be in a position to pick up an issue. 
WIll adding a class attribute `auto_increment_slug` to the Pages Class be a valid solution? 
I added checks to auto-increment if auto_increment_slug is set to `True` in the `clean` method of `CopyForm` class 
```python
if slug and parent_page.get_children().filter(slug=slug).count():
            # If auto_increment_slug of Parent is true, then increment the slug
            if parent_page.auto_increment_slug:
                slug = parent_page._get_autogenerated_slug(slug)
                cleaned_data["new_slug"] = slug
            else:
                self._errors["new_slug"] = self.error_class(
                    [
                        _(
                            'This slug is already in use within the context of its parent page "%(parent_page_title)s"'
                        )
                        % {"parent_page_title": parent_page}
                    ]
                )
                # The slug is no longer valid, hence remove it from cleaned_data
                del cleaned_data["new_slug"]
  ```
I'm not sure if this is the right approach to this
I actually wonder if this is something worth solving in the core code or whether it can just be a documented example leveraging existing Hooks. 

We already have hooks for before copy/create page. I wonder what could be built using this approach alone. 

Determination of the right slug number and when to use that, even if it's driven by an attribute, is something very specific to an application. 

E.g. zero indexed, no number indexed, spelling out numbers, using a-b-c or even a default -copy added to the slug form. Maybe a date gets added automatically. 

If we used the before copy hook, the initial slug on load of the form view could probably just be dynamically updated with a new default value. 

If this can work, it's possible that we could solve this issue with a simple documented example. 
Another take on this could be a way to make the copy page form easier to customise. In theory though you can do this with hooks but it's a few layers of work though. 

See https://stackoverflow.com/questions/55640685/how-do-i-customize-the-wagtail-page-copy-experience (assuming my answer from a while ago is still correct)
I'll look into the hooks to see if they would work
Wouldn't the hook before_publish_page be more suitable than before_copy_page?
What I understood from reading the [hooks doc](https://docs.wagtail.org/en/stable/reference/hooks.html#before-publish-page) is that before_copy_page is used when copying a page where auto-increment might be desirable too but in general, slugs are used to generate URLs when we published right? So wouldn't the former be more desirable?

I might be understanding this wrong but would something like this work:

```python
@hooks.register('before_publish_page')
def auto_increment_slug(page, **kwargs):
    if isinstance(page, YourPageSubclass): 
        base_slug = page.slug
        counter = 1
        new_slug = base_slug
        while Page.objects.filter(slug=new_slug).exclude(id=page.id).exists():
            counter += 1
            new_slug = f"{base_slug}-{counter}"
        page.slug = new_slug
```
@NXPY123 I think the original use case was scenarios where you have an existing page, you go to copy it, the user has to manually change the slug.

In my contrived example here I have blog post 5 and I went to copy it, it would be nice if the slug was pre-filled on this page with `wild-yeast-6`. The current behaviour is to copy the current slug, but this will cause a duplicate slug error unless the user changes it.

<img width="1295" alt="Screenshot 2023-10-11 at 6 19 25 pm" src="https://github.com/wagtail/wagtail/assets/1396140/bda7c78f-0f12-4cbb-9f59-6e8620e74b56">

In the case of creation of a new page, the user would have already entered a title and the slug is based on that, so maybe that's a different case (if the slug matches, allow a different value to be auto-set).


--- 


I think your hook suggestion could work for this second use case, but you'll need to check when the Page creation validation runs. My guess is that the publish page hook is too late. 

Maybe next steps are to try out some code, now that you've got two.use cases and see what works and what breaks. 
I think the before_copy_page hook might not work as I tried to change the slug in the request/page object being passed to the copy View in wagtail/admin/views/pages but I wasn't able to do it. I think it's only possible for the hook to execute some code before the view is called. 

This is what I tried based on info provided by other Wagtail developers:
```python
@hooks.register("before_copy_page")
def safe_slugify(request,page):
    """
    Autogenerate a slug for a page that is guaranteed to be unique for its parent.
    """
    parent_page = page.get_parent()

    suffix = 2
    if page.slug:
        try:
            suffix = int(page.slug[-1])+1
            base_slug = page.slug[:-2]
            
        except ValueError:
            base_slug = page.slug

    candidate_slug = base_slug + f"-{suffix}"
    while not Page._slug_is_available(candidate_slug, parent_page):
        suffix += 1
        candidate_slug = f"{base_slug}-{suffix}"
```

The thing is in the docs of the before_create_page hook an example of altering view for a particular Page is given as:
```python
from wagtail import hooks

from .models import AwesomePage
from .admin_views import edit_awesome_page

@hooks.register('before_create_page')
def before_create_page(request, parent_page, page_class):
    # Use a custom create view for the AwesomePage model
    if page_class == AwesomePage:
        return create_awesome_page(request, parent_page)
```
But trying return custom_view(request,parent_page) just leads to maximum recursion depth exceeded for before_copy_page hook.
@lb- 

I was able to auto-increment through the following way:

admin/views/pages/copy.py
```python
@user_passes_test(user_has_any_page_permission)
def customCopy(request, page_id):
    page = Page.objects.get(id=page_id)

    # Parent page defaults to parent of source page
    parent_page = page.get_parent()

    # Check if the user has permission to publish subpages on the parent
    can_publish = parent_page.permissions_for_user(request.user).can_publish_subpage()

    # Create the form
    form = CustomCopyForm(
        request.POST or None, user=request.user, page=page, can_publish=can_publish, 
    )

    next_url = get_valid_next_url_from_request(request)

    for fn in hooks.get_hooks("before_copy_page"):
        result = fn(request, page)
        if hasattr(result, "status_code"):
            return result

    # Check if user is submitting
    if request.method == "POST":
        # Prefill parent_page in case the form is invalid (as prepopulated value for the form field,
        # because ModelChoiceField seems to not fall back to the user given value)
        parent_page = Page.objects.get(id=request.POST["new_parent_page"])

        if form.is_valid():
            # Receive the parent page (this should never be empty)
            if form.cleaned_data["new_parent_page"]:
                parent_page = form.cleaned_data["new_parent_page"]

            # Re-check if the user has permission to publish subpages on the new parent
            can_publish = parent_page.permissions_for_user(
                request.user
            ).can_publish_subpage()
            keep_live = can_publish and form.cleaned_data.get("publish_copies")

            # Copy the page
            # Note that only users who can publish in the new parent page can create an alias.
            # This is because alias pages must always match their original page's state.
            if can_publish and form.cleaned_data.get("alias"):
                action = CreatePageAliasAction(
                    page.specific,
                    recursive=form.cleaned_data.get("copy_subpages"),
                    parent=parent_page,
                    update_slug=form.cleaned_data["new_slug"],
                    user=request.user,
                )
                new_page = action.execute(skip_permission_checks=True)
            else:
                action = CopyPageAction(
                    page=page,
                    recursive=form.cleaned_data.get("copy_subpages"),
                    to=parent_page,
                    update_attrs={
                        "title": form.cleaned_data["new_title"],
                        "slug": form.cleaned_data["new_slug"],
                    },
                    keep_live=keep_live,
                    user=request.user,
                )
                new_page = action.execute()

            # Give a success message back to the user
            if form.cleaned_data.get("copy_subpages"):
                messages.success(
                    request,
                    _("Page '%(page_title)s' and %(subpages_count)s subpages copied.")
                    % {
                        "page_title": page.specific_deferred.get_admin_display_title(),
                        "subpages_count": new_page.get_descendants().count(),
                    },
                )
            else:
                messages.success(
                    request,
                    _("Page '%(page_title)s' copied.")
                    % {"page_title": page.specific_deferred.get_admin_display_title()},
                )

            for fn in hooks.get_hooks("after_copy_page"):
                result = fn(request, page, new_page)
                if hasattr(result, "status_code"):
                    return result

            # Redirect to explore of parent page
            if next_url:
                return redirect(next_url)
            return redirect("wagtailadmin_explore", parent_page.id)

    return TemplateResponse(
        request,
        "wagtailadmin/pages/copy.html",
        {
            "page": page,
            "form": form,
            "next": next_url,
        },
    )
```
admin/urls/pages.py
```python
#.. Rest of the code
#path("<int:page_id>/copy/", copy.copy, name="copy"),
path("<int:page_id>/copy/", copy.customCopy, name="copy"),
#.. Rest of the code
```

admin/forms/pages.py
```python
class CustomCopyForm(CopyForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        suffix = 2
        parent_page = self.page.get_parent()
        if self.page.slug:
            try:
                suffix = int(self.page.slug[-1])+1
                base_slug = self.page.slug[:-2]
                
            except ValueError:
                base_slug = self.page.slug

        candidate_slug = base_slug + f"-{suffix}"
        while not Page._slug_is_available(candidate_slug, parent_page):
            suffix += 1
            candidate_slug = f"{base_slug}-{suffix}"
            candidate_slug
        allow_unicode = getattr(settings, "WAGTAIL_ALLOW_UNICODE_SLUGS", True)
        self.fields["new_slug"] = forms.SlugField(
            initial=candidate_slug,
            label=_("New slug"),
            allow_unicode=allow_unicode,
            widget=widgets.slug.SlugInput,
        )
```

When I call copy page, I get this:
<img width="1320" alt="image" src="https://github.com/wagtail/wagtail/assets/46917698/0c5e8f50-6fe9-4139-88b9-d7c5981f6511">



This seems like an unnecessarily complicated way to do it though. I think it might be possible to do it with the hooks, I'll look into that again. The only issue is the hooks are called after the form is created in the view:

admin/views/pages/copy.py
```python
@user_passes_test(user_has_any_page_permission)
def copy(request, page_id):
    page = Page.objects.get(id=page_id)

    # Parent page defaults to parent of source page
    parent_page = page.get_parent()

    # Check if the user has permission to publish subpages on the parent
    can_publish = parent_page.permissions_for_user(request.user).can_publish_subpage()

    # Create the form
    form = CopyForm(
        request.POST or None, user=request.user, page=page, can_publish=can_publish
    )

    next_url = get_valid_next_url_from_request(request)

    for fn in hooks.get_hooks("before_copy_page"):
        result = fn(request, page)
        if hasattr(result, "status_code"):
            return result

    # Check if user is submitting
    if request.method == "POST":
       # Rest of the code
```
Wouldn't this mean the hooks can't alter the slug before it is intialized in the form?
I wonder if a smaller code change would make it easier to support more customisations of the CopyForm. 

Let's say we passed in the `form` to the before copy hook here. 

https://github.com/wagtail/wagtail/blob/c13edd0e423eebcdeb9e01beb2bf2939efcd6411/wagtail/admin/views/pages/copy.py#L29
>        result = fn(request, page)

This would enable a fair bit of changes to the generated CopyForm instance. 

Instead of us having to solve for a complex requirement of auto incrementing, and a complex and fragile documented override we can meet in the middle. Support a wider range of changes with the existing hook and documentation of an example using a simple auto-increment slug value. 

It may also pave the way to solve / suggest workarounds for
- https://github.com/wagtail/wagtail/issues/5722
- https://github.com/wagtail/wagtail/issues/7773
- https://github.com/wagtail/wagtail/issues/5966

Maybe locally, see what you can do with a one line chage to your version of Wagtail that passes the form through the hook also. 


```diff
    # Create the form
    form = CopyForm(
        request.POST or None, user=request.user, page=page, can_publish=can_publish
    )

    next_url = get_valid_next_url_from_request(request)

    for fn in hooks.get_hooks("before_copy_page"):
-        result = fn(request, page)
+        result = fn(request, page, form)
        if hasattr(result, "status_code"):
            return result

```





Thanks for the idea! I tried passing the form but I couldn't quite get it to work. But I set the new slug as a return value from the hook and it works. I kinda misunderstood when the before_copy_page hooks were called.

admin/views/pages/copy.py
```python
@user_passes_test(user_has_any_page_permission)
def copy(request, page_id):
# Rest of the code
  for fn in hooks.get_hooks("before_copy_page"):
          result = fn(request, page)
          if hasattr(result, "status_code"):
              return result
          # Check if result has slug. If so, update the slug field
          if hasattr(result, "slug"):
              form.fields["new_slug"].initial = result.slug
# Rest of the code
  ```

Edit: Your idea works too when I return the form with the initial of slug field changed in the hook🤦 
wagtail_hooks.py
```python 
def safe_slugify(request,page,form):
# .. Rest of the Code
  form.fields["new_slug"].initial = candidate_slug
  result = {}
  result["form"] = form
  return result
```

admin/views/pages/copy.py
```python
@user_passes_test(user_has_any_page_permission)
def copy(request, page_id):
# Rest of the code
  for fn in hooks.get_hooks("before_copy_page"):
          result = fn(request, page,form)
          if hasattr(result, "status_code"):
              return result
          if hasattr(result,"form"):
              form = result.form
 # Rest of the code
```
OK, assuming we go down the path of adding a new arg to the hook, I go this working and it feels pretty straight forward. This code will work out if there is an existing number at the end (pretty naive) and if there is one it will increment it for the copy form's initial `new_slug` field value.

```py
import re

from wagtail import hooks

@hooks.register('before_copy_page')
def before_create_page(request, page, form):
    def increment(match):
      if match.group(0):
        return str(int(match.group(0)) + 1).zfill(len(match.group(0)))

    new_slug = form.fields['new_slug'].initial
    form.fields['new_slug'].initial = re.sub('([\d]+)$', increment, new_slug)

```

I have not checked whether this will cause issues for the POST request, I think it should be OK as we are only modifying the initial value. Although, a documented snippet would need to consider this.

I think this could be an OK direction but I might flag this change to the `before_copy_page` at the core team meeting to see if others have suggestions first.
That sounds like a nice solution! 
Another approach might be to make it easier for a Page model to control its own CopyForm. Similar to `edit_handler`. 
> Another approach might be to make it easier for a Page model to control its own CopyForm. Similar to `edit_handler`.

Would it be possible to specify the form in the model itself? That might work, but wouldn't the `before_copy_page` hook be easier? Did others give any other suggestions in the meeting?
I'm also following this conversation because we are in need of specifying unique required fields during copy (through the UI) so a way to specify a custom CopyForm would be a great help.
Are there any updates on this?
@NXPY123 I think the idea of the hook approach is not going the suitable, passing around the form object to the hook is not ideal. I discussed this briefly on the core team chat. 

So, back to ideas - some kind of method on the page model would be the next avenue.

It would be good to note that there is a PR up for copying snippets - https://github.com/wagtail/wagtail/pull/11270


How about we add this is in the `CopyForm`:
```
if self.page.auto_increment_slug:
            suffix = 2
            parent_page = self.page.get_parent()
            if self.page.slug:
                try:
                    suffix = int(self.page.slug[-1])+1
                    base_slug = self.page.slug[:-2]
                    
                except ValueError:
                    base_slug = self.page.slug

            candidate_slug = base_slug + f"-{suffix}"
            while not Page._slug_is_available(candidate_slug, parent_page):
                suffix += 1
                candidate_slug = f"{base_slug}-{suffix}"
                candidate_slug
            self.fields["new_slug"] = forms.SlugField(
                initial=candidate_slug,
                label=_("New slug"),
                allow_unicode=allow_unicode,
                widget=widgets.slug.SlugInput,
            )

        else:
            self.fields["new_slug"] = forms.SlugField(
                initial=self.page.slug,
                label=_("New slug"),
                allow_unicode=allow_unicode,
                widget=widgets.slug.SlugInput,
            )
```

And just add an`auto_increment_slug` flag in the `Page` Model?
@NXPY123 my concern is that this will solve for a subset of the use case for auto slugs on copy forms but may not consider some use cases. 

I still think having a way to provide a custom CopyForm is the right way to go. Then we can have an example in the documentation of using this to make your own auto-increment slug default value. 

This way we (core code) doesn't have to solve all the edge cases, it leaves the usage to individual developers to solve for what they need. In time there may be packages and a bit of convergence on a robust way to do this. But if it just stays as a niche use case, we've made it possible to do this in Wagtail with only a small core code maintenance overhead.


----

Having a new section in this page makes Sense to me. 

https://docs.wagtail.org/en/stable/advanced_topics/customisation/page_editing_interface.html#customising-generated-forms


`copy_form_class`  could be a suitable as attribute name. 

This sounds like a brilliant idea!

Did you mean like this:
`wagtail/admin/views/pages/copy.py`
```
def copy(request, page_id):
    form_class = getattr(page.specific_class, "copy_form_class", CopyForm)
```

`models.py`
```
from django.db import models
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import (
    FieldPanel,
    HelpPanel,
    MultiFieldPanel,
    MultipleChooserPanel,
)
from wagtail.fields import RichTextField, StreamField
from wagtail.models import Orderable, Page
from wagtail.search import index
from wagtail.admin.forms.pages import CopyForm
from wagtail.admin import widgets
from django.conf import settings
from django.utils.translation import gettext as _
from django import forms
from bakerydemo.base.blocks import BaseStreamBlock

class CustomCopyForm(CopyForm):
    def __init__(self, *args, **kwargs):
        # call super
        super().__init__(*args, **kwargs)
        # set initial_slug as incremented slug
        suffix = 2
        parent_page = self.page.get_parent()
        if self.page.slug:
            try:
                suffix = int(self.page.slug[-1])+1
                base_slug = self.page.slug[:-2]
                
            except ValueError:
                base_slug = self.page.slug

        candidate_slug = base_slug + f"-{suffix}"
        while not Page._slug_is_available(candidate_slug, parent_page):
            suffix += 1
            candidate_slug = f"{base_slug}-{suffix}"
            candidate_slug
        allow_unicode = getattr(settings, "WAGTAIL_ALLOW_UNICODE_SLUGS", True)
        self.fields["new_slug"] = forms.SlugField(
            initial=candidate_slug,
            label=_("New slug"),
            allow_unicode=allow_unicode,
            widget=widgets.slug.SlugInput,
        )

class RecipePage(Page):
    """
    Recipe pages are more complex than blog pages, demonstrating more advanced StreamField patterns.
    """
    copy_form_class = CustomCopyForm
```
Yep. That's the idea @NXPY123 
Can I start making a PR with this? I tried it out and it does work.
@NXPY123 go for it. 
@lb-  I want to give it a try, 
@suyash5053  may I suggest you keep trying to get to the bottom of https://github.com/wagtail/wagtail/pull/10122 and see if you can finish that off before tacking this issue (which will likely involve a lot more complexity and unit testing scenarios).

You are free to help with whatever you want, but it's often better to try to focus on one thing at a time.
Your proposal to add an `auto_increment_slug` meta flag to page models, which would allow for automatic generation of incremented slugs in cases of duplicate URLs, is a reasonable solution to the problem you've described.

To address the edge cases you've mentioned, it would be necessary to have additional logic to determine whether the conflicting path part is actually an incremental number. This could be done using regular expressions or other pattern matching techniques.

It's worth noting that this feature would only be useful for certain types of page models where slug uniqueness is not critical, and where having a duplicate slug with an incremented number would still be acceptable. Therefore, it would be important to make it clear in the documentation that this is an optional feature that should only be used in appropriate situations.

Please assign me this issue, I am Interested.
@AnuragMishraa please try to add constructive comments, I've hidden your message for now as it basically repeats the issue. 

Once you feel confident with contributing and have followed the steps on https://github.com/wagtail/wagtail/issues/10070 you will hopefully be in a position to pick up an issue. 
WIll adding a class attribute `auto_increment_slug` to the Pages Class be a valid solution? 
I added checks to auto-increment if auto_increment_slug is set to `True` in the `clean` method of `CopyForm` class 
```python
if slug and parent_page.get_children().filter(slug=slug).count():
            # If auto_increment_slug of Parent is true, then increment the slug
            if parent_page.auto_increment_slug:
                slug = parent_page._get_autogenerated_slug(slug)
                cleaned_data["new_slug"] = slug
            else:
                self._errors["new_slug"] = self.error_class(
                    [
                        _(
                            'This slug is already in use within the context of its parent page "%(parent_page_title)s"'
                        )
                        % {"parent_page_title": parent_page}
                    ]
                )
                # The slug is no longer valid, hence remove it from cleaned_data
                del cleaned_data["new_slug"]
  ```
I'm not sure if this is the right approach to this
I actually wonder if this is something worth solving in the core code or whether it can just be a documented example leveraging existing Hooks. 

We already have hooks for before copy/create page. I wonder what could be built using this approach alone. 

Determination of the right slug number and when to use that, even if it's driven by an attribute, is something very specific to an application. 

E.g. zero indexed, no number indexed, spelling out numbers, using a-b-c or even a default -copy added to the slug form. Maybe a date gets added automatically. 

If we used the before copy hook, the initial slug on load of the form view could probably just be dynamically updated with a new default value. 

If this can work, it's possible that we could solve this issue with a simple documented example. 
Another take on this could be a way to make the copy page form easier to customise. In theory though you can do this with hooks but it's a few layers of work though. 

See https://stackoverflow.com/questions/55640685/how-do-i-customize-the-wagtail-page-copy-experience (assuming my answer from a while ago is still correct)
I'll look into the hooks to see if they would work
Wouldn't the hook before_publish_page be more suitable than before_copy_page?
What I understood from reading the [hooks doc](https://docs.wagtail.org/en/stable/reference/hooks.html#before-publish-page) is that before_copy_page is used when copying a page where auto-increment might be desirable too but in general, slugs are used to generate URLs when we published right? So wouldn't the former be more desirable?

I might be understanding this wrong but would something like this work:

```python
@hooks.register('before_publish_page')
def auto_increment_slug(page, **kwargs):
    if isinstance(page, YourPageSubclass): 
        base_slug = page.slug
        counter = 1
        new_slug = base_slug
        while Page.objects.filter(slug=new_slug).exclude(id=page.id).exists():
            counter += 1
            new_slug = f"{base_slug}-{counter}"
        page.slug = new_slug
```
@NXPY123 I think the original use case was scenarios where you have an existing page, you go to copy it, the user has to manually change the slug.

In my contrived example here I have blog post 5 and I went to copy it, it would be nice if the slug was pre-filled on this page with `wild-yeast-6`. The current behaviour is to copy the current slug, but this will cause a duplicate slug error unless the user changes it.

<img width="1295" alt="Screenshot 2023-10-11 at 6 19 25 pm" src="https://github.com/wagtail/wagtail/assets/1396140/bda7c78f-0f12-4cbb-9f59-6e8620e74b56">

In the case of creation of a new page, the user would have already entered a title and the slug is based on that, so maybe that's a different case (if the slug matches, allow a different value to be auto-set).


--- 


I think your hook suggestion could work for this second use case, but you'll need to check when the Page creation validation runs. My guess is that the publish page hook is too late. 

Maybe next steps are to try out some code, now that you've got two.use cases and see what works and what breaks. 
I think the before_copy_page hook might not work as I tried to change the slug in the request/page object being passed to the copy View in wagtail/admin/views/pages but I wasn't able to do it. I think it's only possible for the hook to execute some code before the view is called. 

This is what I tried based on info provided by other Wagtail developers:
```python
@hooks.register("before_copy_page")
def safe_slugify(request,page):
    """
    Autogenerate a slug for a page that is guaranteed to be unique for its parent.
    """
    parent_page = page.get_parent()

    suffix = 2
    if page.slug:
        try:
            suffix = int(page.slug[-1])+1
            base_slug = page.slug[:-2]
            
        except ValueError:
            base_slug = page.slug

    candidate_slug = base_slug + f"-{suffix}"
    while not Page._slug_is_available(candidate_slug, parent_page):
        suffix += 1
        candidate_slug = f"{base_slug}-{suffix}"
```

The thing is in the docs of the before_create_page hook an example of altering view for a particular Page is given as:
```python
from wagtail import hooks

from .models import AwesomePage
from .admin_views import edit_awesome_page

@hooks.register('before_create_page')
def before_create_page(request, parent_page, page_class):
    # Use a custom create view for the AwesomePage model
    if page_class == AwesomePage:
        return create_awesome_page(request, parent_page)
```
But trying return custom_view(request,parent_page) just leads to maximum recursion depth exceeded for before_copy_page hook.
@lb- 

I was able to auto-increment through the following way:

admin/views/pages/copy.py
```python
@user_passes_test(user_has_any_page_permission)
def customCopy(request, page_id):
    page = Page.objects.get(id=page_id)

    # Parent page defaults to parent of source page
    parent_page = page.get_parent()

    # Check if the user has permission to publish subpages on the parent
    can_publish = parent_page.permissions_for_user(request.user).can_publish_subpage()

    # Create the form
    form = CustomCopyForm(
        request.POST or None, user=request.user, page=page, can_publish=can_publish, 
    )

    next_url = get_valid_next_url_from_request(request)

    for fn in hooks.get_hooks("before_copy_page"):
        result = fn(request, page)
        if hasattr(result, "status_code"):
            return result

    # Check if user is submitting
    if request.method == "POST":
        # Prefill parent_page in case the form is invalid (as prepopulated value for the form field,
        # because ModelChoiceField seems to not fall back to the user given value)
        parent_page = Page.objects.get(id=request.POST["new_parent_page"])

        if form.is_valid():
            # Receive the parent page (this should never be empty)
            if form.cleaned_data["new_parent_page"]:
                parent_page = form.cleaned_data["new_parent_page"]

            # Re-check if the user has permission to publish subpages on the new parent
            can_publish = parent_page.permissions_for_user(
                request.user
            ).can_publish_subpage()
            keep_live = can_publish and form.cleaned_data.get("publish_copies")

            # Copy the page
            # Note that only users who can publish in the new parent page can create an alias.
            # This is because alias pages must always match their original page's state.
            if can_publish and form.cleaned_data.get("alias"):
                action = CreatePageAliasAction(
                    page.specific,
                    recursive=form.cleaned_data.get("copy_subpages"),
                    parent=parent_page,
                    update_slug=form.cleaned_data["new_slug"],
                    user=request.user,
                )
                new_page = action.execute(skip_permission_checks=True)
            else:
                action = CopyPageAction(
                    page=page,
                    recursive=form.cleaned_data.get("copy_subpages"),
                    to=parent_page,
                    update_attrs={
                        "title": form.cleaned_data["new_title"],
                        "slug": form.cleaned_data["new_slug"],
                    },
                    keep_live=keep_live,
                    user=request.user,
                )
                new_page = action.execute()

            # Give a success message back to the user
            if form.cleaned_data.get("copy_subpages"):
                messages.success(
                    request,
                    _("Page '%(page_title)s' and %(subpages_count)s subpages copied.")
                    % {
                        "page_title": page.specific_deferred.get_admin_display_title(),
                        "subpages_count": new_page.get_descendants().count(),
                    },
                )
            else:
                messages.success(
                    request,
                    _("Page '%(page_title)s' copied.")
                    % {"page_title": page.specific_deferred.get_admin_display_title()},
                )

            for fn in hooks.get_hooks("after_copy_page"):
                result = fn(request, page, new_page)
                if hasattr(result, "status_code"):
                    return result

            # Redirect to explore of parent page
            if next_url:
                return redirect(next_url)
            return redirect("wagtailadmin_explore", parent_page.id)

    return TemplateResponse(
        request,
        "wagtailadmin/pages/copy.html",
        {
            "page": page,
            "form": form,
            "next": next_url,
        },
    )
```
admin/urls/pages.py
```python
#.. Rest of the code
#path("<int:page_id>/copy/", copy.copy, name="copy"),
path("<int:page_id>/copy/", copy.customCopy, name="copy"),
#.. Rest of the code
```

admin/forms/pages.py
```python
class CustomCopyForm(CopyForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        suffix = 2
        parent_page = self.page.get_parent()
        if self.page.slug:
            try:
                suffix = int(self.page.slug[-1])+1
                base_slug = self.page.slug[:-2]
                
            except ValueError:
                base_slug = self.page.slug

        candidate_slug = base_slug + f"-{suffix}"
        while not Page._slug_is_available(candidate_slug, parent_page):
            suffix += 1
            candidate_slug = f"{base_slug}-{suffix}"
            candidate_slug
        allow_unicode = getattr(settings, "WAGTAIL_ALLOW_UNICODE_SLUGS", True)
        self.fields["new_slug"] = forms.SlugField(
            initial=candidate_slug,
            label=_("New slug"),
            allow_unicode=allow_unicode,
            widget=widgets.slug.SlugInput,
        )
```

When I call copy page, I get this:
<img width="1320" alt="image" src="https://github.com/wagtail/wagtail/assets/46917698/0c5e8f50-6fe9-4139-88b9-d7c5981f6511">



This seems like an unnecessarily complicated way to do it though. I think it might be possible to do it with the hooks, I'll look into that again. The only issue is the hooks are called after the form is created in the view:

admin/views/pages/copy.py
```python
@user_passes_test(user_has_any_page_permission)
def copy(request, page_id):
    page = Page.objects.get(id=page_id)

    # Parent page defaults to parent of source page
    parent_page = page.get_parent()

    # Check if the user has permission to publish subpages on the parent
    can_publish = parent_page.permissions_for_user(request.user).can_publish_subpage()

    # Create the form
    form = CopyForm(
        request.POST or None, user=request.user, page=page, can_publish=can_publish
    )

    next_url = get_valid_next_url_from_request(request)

    for fn in hooks.get_hooks("before_copy_page"):
        result = fn(request, page)
        if hasattr(result, "status_code"):
            return result

    # Check if user is submitting
    if request.method == "POST":
       # Rest of the code
```
Wouldn't this mean the hooks can't alter the slug before it is intialized in the form?
I wonder if a smaller code change would make it easier to support more customisations of the CopyForm. 

Let's say we passed in the `form` to the before copy hook here. 

https://github.com/wagtail/wagtail/blob/c13edd0e423eebcdeb9e01beb2bf2939efcd6411/wagtail/admin/views/pages/copy.py#L29
>        result = fn(request, page)

This would enable a fair bit of changes to the generated CopyForm instance. 

Instead of us having to solve for a complex requirement of auto incrementing, and a complex and fragile documented override we can meet in the middle. Support a wider range of changes with the existing hook and documentation of an example using a simple auto-increment slug value. 

It may also pave the way to solve / suggest workarounds for
- https://github.com/wagtail/wagtail/issues/5722
- https://github.com/wagtail/wagtail/issues/7773
- https://github.com/wagtail/wagtail/issues/5966

Maybe locally, see what you can do with a one line chage to your version of Wagtail that passes the form through the hook also. 


```diff
    # Create the form
    form = CopyForm(
        request.POST or None, user=request.user, page=page, can_publish=can_publish
    )

    next_url = get_valid_next_url_from_request(request)

    for fn in hooks.get_hooks("before_copy_page"):
-        result = fn(request, page)
+        result = fn(request, page, form)
        if hasattr(result, "status_code"):
            return result

```





Thanks for the idea! I tried passing the form but I couldn't quite get it to work. But I set the new slug as a return value from the hook and it works. I kinda misunderstood when the before_copy_page hooks were called.

admin/views/pages/copy.py
```python
@user_passes_test(user_has_any_page_permission)
def copy(request, page_id):
# Rest of the code
  for fn in hooks.get_hooks("before_copy_page"):
          result = fn(request, page)
          if hasattr(result, "status_code"):
              return result
          # Check if result has slug. If so, update the slug field
          if hasattr(result, "slug"):
              form.fields["new_slug"].initial = result.slug
# Rest of the code
  ```

Edit: Your idea works too when I return the form with the initial of slug field changed in the hook🤦 
wagtail_hooks.py
```python 
def safe_slugify(request,page,form):
# .. Rest of the Code
  form.fields["new_slug"].initial = candidate_slug
  result = {}
  result["form"] = form
  return result
```

admin/views/pages/copy.py
```python
@user_passes_test(user_has_any_page_permission)
def copy(request, page_id):
# Rest of the code
  for fn in hooks.get_hooks("before_copy_page"):
          result = fn(request, page,form)
          if hasattr(result, "status_code"):
              return result
          if hasattr(result,"form"):
              form = result.form
 # Rest of the code
```
OK, assuming we go down the path of adding a new arg to the hook, I go this working and it feels pretty straight forward. This code will work out if there is an existing number at the end (pretty naive) and if there is one it will increment it for the copy form's initial `new_slug` field value.

```py
import re

from wagtail import hooks

@hooks.register('before_copy_page')
def before_create_page(request, page, form):
    def increment(match):
      if match.group(0):
        return str(int(match.group(0)) + 1).zfill(len(match.group(0)))

    new_slug = form.fields['new_slug'].initial
    form.fields['new_slug'].initial = re.sub('([\d]+)$', increment, new_slug)

```

I have not checked whether this will cause issues for the POST request, I think it should be OK as we are only modifying the initial value. Although, a documented snippet would need to consider this.

I think this could be an OK direction but I might flag this change to the `before_copy_page` at the core team meeting to see if others have suggestions first.
That sounds like a nice solution! 
Another approach might be to make it easier for a Page model to control its own CopyForm. Similar to `edit_handler`. 
> Another approach might be to make it easier for a Page model to control its own CopyForm. Similar to `edit_handler`.

Would it be possible to specify the form in the model itself? That might work, but wouldn't the `before_copy_page` hook be easier? Did others give any other suggestions in the meeting?
I'm also following this conversation because we are in need of specifying unique required fields during copy (through the UI) so a way to specify a custom CopyForm would be a great help.
Are there any updates on this?
@NXPY123 I think the idea of the hook approach is not going the suitable, passing around the form object to the hook is not ideal. I discussed this briefly on the core team chat. 

So, back to ideas - some kind of method on the page model would be the next avenue.

It would be good to note that there is a PR up for copying snippets - https://github.com/wagtail/wagtail/pull/11270


How about we add this is in the `CopyForm`:
```
if self.page.auto_increment_slug:
            suffix = 2
            parent_page = self.page.get_parent()
            if self.page.slug:
                try:
                    suffix = int(self.page.slug[-1])+1
                    base_slug = self.page.slug[:-2]
                    
                except ValueError:
                    base_slug = self.page.slug

            candidate_slug = base_slug + f"-{suffix}"
            while not Page._slug_is_available(candidate_slug, parent_page):
                suffix += 1
                candidate_slug = f"{base_slug}-{suffix}"
                candidate_slug
            self.fields["new_slug"] = forms.SlugField(
                initial=candidate_slug,
                label=_("New slug"),
                allow_unicode=allow_unicode,
                widget=widgets.slug.SlugInput,
            )

        else:
            self.fields["new_slug"] = forms.SlugField(
                initial=self.page.slug,
                label=_("New slug"),
                allow_unicode=allow_unicode,
                widget=widgets.slug.SlugInput,
            )
```

And just add an`auto_increment_slug` flag in the `Page` Model?
@NXPY123 my concern is that this will solve for a subset of the use case for auto slugs on copy forms but may not consider some use cases. 

I still think having a way to provide a custom CopyForm is the right way to go. Then we can have an example in the documentation of using this to make your own auto-increment slug default value. 

This way we (core code) doesn't have to solve all the edge cases, it leaves the usage to individual developers to solve for what they need. In time there may be packages and a bit of convergence on a robust way to do this. But if it just stays as a niche use case, we've made it possible to do this in Wagtail with only a small core code maintenance overhead.


----

Having a new section in this page makes Sense to me. 

https://docs.wagtail.org/en/stable/advanced_topics/customisation/page_editing_interface.html#customising-generated-forms


`copy_form_class`  could be a suitable as attribute name. 

This sounds like a brilliant idea!

Did you mean like this:
`wagtail/admin/views/pages/copy.py`
```
def copy(request, page_id):
    form_class = getattr(page.specific_class, "copy_form_class", CopyForm)
```

`models.py`
```
from django.db import models
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import (
    FieldPanel,
    HelpPanel,
    MultiFieldPanel,
    MultipleChooserPanel,
)
from wagtail.fields import RichTextField, StreamField
from wagtail.models import Orderable, Page
from wagtail.search import index
from wagtail.admin.forms.pages import CopyForm
from wagtail.admin import widgets
from django.conf import settings
from django.utils.translation import gettext as _
from django import forms
from bakerydemo.base.blocks import BaseStreamBlock

class CustomCopyForm(CopyForm):
    def __init__(self, *args, **kwargs):
        # call super
        super().__init__(*args, **kwargs)
        # set initial_slug as incremented slug
        suffix = 2
        parent_page = self.page.get_parent()
        if self.page.slug:
            try:
                suffix = int(self.page.slug[-1])+1
                base_slug = self.page.slug[:-2]
                
            except ValueError:
                base_slug = self.page.slug

        candidate_slug = base_slug + f"-{suffix}"
        while not Page._slug_is_available(candidate_slug, parent_page):
            suffix += 1
            candidate_slug = f"{base_slug}-{suffix}"
            candidate_slug
        allow_unicode = getattr(settings, "WAGTAIL_ALLOW_UNICODE_SLUGS", True)
        self.fields["new_slug"] = forms.SlugField(
            initial=candidate_slug,
            label=_("New slug"),
            allow_unicode=allow_unicode,
            widget=widgets.slug.SlugInput,
        )

class RecipePage(Page):
    """
    Recipe pages are more complex than blog pages, demonstrating more advanced StreamField patterns.
    """
    copy_form_class = CustomCopyForm
```
Yep. That's the idea @NXPY123 
Can I start making a PR with this? I tried it out and it does work.
@NXPY123 go for it. 

Resolving patch:
diff --git a/wagtail/admin/views/pages/copy.py b/wagtail/admin/views/pages/copy.py
--- a/wagtail/admin/views/pages/copy.py
+++ b/wagtail/admin/views/pages/copy.py
@@ -22,8 +22,8 @@ def copy(request, page_id):
     # Check if the user has permission to publish subpages on the parent
     can_publish = parent_page.permissions_for_user(request.user).can_publish_subpage()
 
-    # Create the form
-    form = CopyForm(
+    form_class = getattr(page.specific_class, "copy_form_class", CopyForm)
+    form = form_class(
         request.POST or None, user=request.user, page=page, can_publish=can_publish
     )
 


Test patch:
diff --git a/wagtail/admin/tests/pages/test_copy_page.py b/wagtail/admin/tests/pages/test_copy_page.py
--- a/wagtail/admin/tests/pages/test_copy_page.py
+++ b/wagtail/admin/tests/pages/test_copy_page.py
@@ -5,6 +5,7 @@
 
 from wagtail.models import GroupPagePermission, Page
 from wagtail.test.testapp.models import (
+    CustomCopyFormPage,
     EventPage,
     EventPageSpeaker,
     PageWithExcludedCopyField,
@@ -781,3 +782,22 @@ def test_copy_page_with_unique_uuids_in_orderables(self):
             event_page.speakers.first().translation_key,
             new_page.speakers.first().translation_key,
         )
+
+    def test_page_copy_with_custom_copy_form(self):
+        custom_copy_form_page = self.root_page.add_child(
+            instance=CustomCopyFormPage(
+                title="Hello world!",
+                slug="copy-form",
+                live=True,
+                has_unpublished_changes=False,
+            )
+        )
+
+        response = self.client.get(
+            reverse("wagtailadmin_pages:copy", args=(custom_copy_form_page.id,))
+        )
+        # Check response
+        self.assertEqual(response.status_code, 200)
+
+        # Check if slug is hello-world-2
+        self.assertContains(response, "copy-form-2")
diff --git a/wagtail/test/testapp/migrations/0033_customcopyformpage.py b/wagtail/test/testapp/migrations/0033_customcopyformpage.py
new file mode 100644
--- /dev/null
+++ b/wagtail/test/testapp/migrations/0033_customcopyformpage.py
@@ -0,0 +1,35 @@
+# Generated by Django 4.2.5 on 2023-12-30 06:09
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("wagtailcore", "0091_remove_revision_submitted_for_moderation"),
+        ("tests", "0032_custompermissionpage"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="CustomCopyFormPage",
+            fields=[
+                (
+                    "page_ptr",
+                    models.OneToOneField(
+                        auto_created=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        parent_link=True,
+                        primary_key=True,
+                        serialize=False,
+                        to="wagtailcore.page",
+                    ),
+                ),
+            ],
+            options={
+                "abstract": False,
+            },
+            bases=("wagtailcore.page",),
+        ),
+    ]
diff --git a/wagtail/test/testapp/models.py b/wagtail/test/testapp/models.py
--- a/wagtail/test/testapp/models.py
+++ b/wagtail/test/testapp/models.py
@@ -21,7 +21,9 @@
 from taggit.managers import TaggableManager
 from taggit.models import ItemBase, TagBase, TaggedItemBase
 
+from wagtail.admin import widgets
 from wagtail.admin.forms import WagtailAdminPageForm
+from wagtail.admin.forms.pages import CopyForm
 from wagtail.admin.mail import send_mail
 from wagtail.admin.panels import (
     FieldPanel,
@@ -574,6 +576,38 @@ def get_context(self, request):
     ]
 
 
+# CopyForm allowing auto-increment of slugs
+
+
+class CustomCopyForm(CopyForm):
+    def __init__(self, *args, **kwargs):
+        # call super
+        super().__init__(*args, **kwargs)
+        # set initial_slug as incremented slug
+        suffix = 2
+        parent_page = self.page.get_parent()
+        if self.page.slug:
+            try:
+                suffix = int(self.page.slug[-1]) + 1
+                base_slug = self.page.slug[:-2]
+
+            except ValueError:
+                base_slug = self.page.slug
+
+        candidate_slug = base_slug + f"-{suffix}"
+        while not Page._slug_is_available(candidate_slug, parent_page):
+            suffix += 1
+            candidate_slug = f"{base_slug}-{suffix}"
+            candidate_slug
+        allow_unicode = getattr(settings, "WAGTAIL_ALLOW_UNICODE_SLUGS", True)
+        self.fields["new_slug"] = forms.SlugField(
+            initial=candidate_slug,
+            label=_("New slug"),
+            allow_unicode=allow_unicode,
+            widget=widgets.slug.SlugInput,
+        )
+
+
 # FormPage with a non-HTML extension
 
 
@@ -1331,6 +1365,10 @@ class BusinessNowherePage(Page):
     parent_page_types = []
 
 
+class CustomCopyFormPage(Page):
+    copy_form_class = CustomCopyForm
+
+
 class TaggedPageTag(TaggedItemBase):
     content_object = ParentalKey(
         "tests.TaggedPage", related_name="tagged_items", on_delete=models.CASCADE