diff --git a/.editorconfig b/.editorconfig
index 8ae05aa..b3561a7 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,9 +1,6 @@
-# Generated from:
-# https://github.com/plone/meta/tree/master/config/default
-# See the inline comments on how to expand/tweak this configuration file
-#
+
# EditorConfig Configuration file, for more details see:
-# http://EditorConfig.org
+# https://EditorConfig.org
# EditorConfig is a convention description, that could be interpreted
# by multiple editors to enforce common coding conventions for specific
# file types
@@ -22,33 +19,17 @@ trim_trailing_whitespace = true
charset = utf-8
# Indent style default
indent_style = space
-# Max Line Length - a hard line wrap, should be disabled
-max_line_length = off
[*.{py,cfg,ini}]
# 4 space indentation
indent_size = 4
-[*.{yml,zpt,pt,dtml,zcml}]
-# 2 space indentation
-indent_size = 2
-
-[*.{json,jsonl,js,jsx,ts,tsx,css,less,scss,html}] # Frontend development
+[*.{html,dtml,pt,zpt,xml,zcml,js,json,ts,less,scss,css,sass,yml,yaml}]
# 2 space indentation
indent_size = 2
-max_line_length = 80
[{Makefile,.gitmodules}]
# Tab indentation (no size specified, but view as 4 spaces)
indent_style = tab
indent_size = unset
tab_width = unset
-
-
-##
-# Add extra configuration options in .meta.toml:
-# [editorconfig]
-# extra_lines = """
-# _your own configuration lines_
-# """
-##
diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml
new file mode 100644
index 0000000..5ed0e35
--- /dev/null
+++ b/.github/workflows/changelog.yml
@@ -0,0 +1,28 @@
+name: Change log check
+on:
+ pull_request:
+ types: [assigned, opened, synchronize, reopened, labeled, unlabeled]
+ branches:
+ - main
+
+env:
+ python-version: 3.13
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ # Fetch all history
+ fetch-depth: '0'
+
+ - name: Setup uv
+ uses: plone/meta/.github/actions/setup_uv@2.x
+ with:
+ python-version: ${{ env.python-version }}
+ - name: Check for presence of a Change Log fragment (only pull requests)
+ if: github.event_name == 'pull_request'
+ run: |
+ git fetch --no-tags origin ${{ github.base_ref }}
+ uvx towncrier check --compare-with origin/${{ github.base_ref }} --config pyproject.toml --dir .
diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml
new file mode 100644
index 0000000..e286a31
--- /dev/null
+++ b/.github/workflows/config.yml
@@ -0,0 +1,70 @@
+name: 'Compute Config variables'
+
+on:
+ workflow_call:
+ inputs:
+ python-version:
+ required: false
+ type: string
+ default: "3.13"
+ plone-version:
+ required: false
+ type: string
+ default: "6.1.4"
+ outputs:
+ backend:
+ description: "Flag reporting if we should run the backend jobs"
+ value: ${{ jobs.config.outputs.backend }}
+ docs:
+ description: "Flag reporting if we should run the docs jobs"
+ value: ${{ jobs.config.outputs.docs }}
+ base-tag:
+ description: "Base tag to be used when creating container images"
+ value: ${{ jobs.config.outputs.base-tag }}
+ python-version:
+ description: "Python version to be used"
+ value: ${{ inputs.python-version }}
+ plone-version:
+ description: "Plone version to be used"
+ value: ${{ inputs.plone-version }}
+
+jobs:
+ config:
+ runs-on: ubuntu-latest
+ outputs:
+ backend: ${{ steps.filter.outputs.backend }}
+ docs: ${{ steps.filter.outputs.docs }}
+ base-tag: ${{ steps.vars.outputs.BASE_TAG }}
+ plone-version: ${{ steps.vars.outputs.plone-version }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+
+ - name: Compute several vars needed for the CI
+ id: vars
+ run: |
+ echo "base-tag=sha-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
+ echo "plone-version=${{ inputs.plone-version }}" >> $GITHUB_OUTPUT
+
+ - uses: dorny/paths-filter@v3.0.2
+ id: filter
+ with:
+ filters: |
+ backend:
+ - '**'
+ - '.github/workflows/config.yml'
+ - '.github/workflows/main.yml'
+ docs:
+ - '.readthedocs.yaml'
+ - 'docs/**'
+ - '.github/workflows/docs.yaml'
+
+ - name: Test vars
+ run: |
+ echo "base-tag: ${{ steps.vars.outputs.base-tag }}"
+ echo 'plone-version: ${{ steps.vars.outputs.plone-version }}'
+ echo 'event-name: ${{ github.event_name }}'
+ echo "ref-name: ${{ github.ref_name }}"
+ echo 'Paths - backend: ${{ steps.filter.outputs.backend }}'
+ echo 'Paths - docs: ${{ steps.filter.outputs.docs }}'
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..9798341
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,61 @@
+name: kitconcept.seo CI
+
+on:
+ push:
+
+jobs:
+
+ config:
+ name: "Compute configuration values"
+ uses: ./.github/workflows/config.yml
+ lint:
+ name: "Lint codebase"
+ uses: plone/meta/.github/workflows/backend-lint.yml@2.x
+ needs:
+ - config
+ with:
+ python-version: ${{ needs.config.outputs.python-version }}
+ plone-version: ${{ needs.config.outputs.plone-version }}
+ test:
+ name: "Test codebase"
+ uses: plone/meta/.github/workflows/backend-pytest.yml@2.x
+ needs:
+ - config
+ strategy:
+ matrix:
+ python-version: ["3.13", "3.12", "3.11", "3.10"]
+ plone-version: ["6.2-latest", "6.1-latest", "6.0-latest"]
+ with:
+ python-version: ${{ matrix.python-version }}
+ plone-version: ${{ matrix.plone-version }}
+
+ coverage:
+ name: "Backend: Coverage"
+ uses: plone/meta/.github/workflows/backend-pytest-coverage.yml@2.x
+ needs:
+ - config
+ - test
+ with:
+ python-version: ${{ needs.config.outputs.python-version }}
+ plone-version: ${{ needs.config.outputs.plone-version }}
+
+ report:
+ name: "Final report"
+ if: ${{ always() }}
+ runs-on: ubuntu-latest
+ needs:
+ - config
+ - lint
+ - test
+ - coverage
+ steps:
+ - name: Report
+ shell: bash
+ run: |
+ echo '# Workflow Report' >> $GITHUB_STEP_SUMMARY
+ echo '| Job ID | Conclusion |' >> $GITHUB_STEP_SUMMARY
+ echo '| --- | --- |' >> $GITHUB_STEP_SUMMARY
+ echo '| Config | ${{ needs.config.result }} |' >> $GITHUB_STEP_SUMMARY
+ echo '| Lint | ${{ needs.coverage.result }} |' >> $GITHUB_STEP_SUMMARY
+ echo '| Test | ${{ needs.coverage.result }} |' >> $GITHUB_STEP_SUMMARY
+ echo '| Coverage | ${{ needs.coverage.result }} |' >> $GITHUB_STEP_SUMMARY
diff --git a/.github/workflows/meta.yml b/.github/workflows/meta.yml
deleted file mode 100644
index 6f55e66..0000000
--- a/.github/workflows/meta.yml
+++ /dev/null
@@ -1,65 +0,0 @@
-# Generated from:
-# https://github.com/plone/meta/tree/master/config/default
-# See the inline comments on how to expand/tweak this configuration file
-name: Meta
-on:
- push:
- branches:
- - master
- - main
- pull_request:
- branches:
- - master
- - main
- workflow_dispatch:
-
-##
-# To set environment variables for all jobs, add in .meta.toml:
-# [github]
-# env = """
-# debug: 1
-# image-name: 'org/image'
-# image-tag: 'latest'
-# """
-##
-
-jobs:
- qa:
- uses: plone/meta/.github/workflows/qa.yml@main
- test:
- uses: plone/meta/.github/workflows/test.yml@main
- coverage:
- uses: plone/meta/.github/workflows/coverage.yml@main
- dependencies:
- uses: plone/meta/.github/workflows/dependencies.yml@main
- release_ready:
- uses: plone/meta/.github/workflows/release_ready.yml@main
-
-##
-# To modify the list of default jobs being created add in .meta.toml:
-# [github]
-# jobs = [
-# "qa",
-# "test",
-# "coverage",
-# "dependencies",
-# "release_ready",
-# "circular",
-# ]
-##
-
-##
-# To request that some OS level dependencies get installed
-# when running tests/coverage jobs, add in .meta.toml:
-# [github]
-# os_dependencies = "git libxml2 libxslt"
-##
-
-##
-# Specify additional jobs in .meta.toml:
-# [github]
-# extra_lines = """
-# another:
-# uses: org/repo/.github/workflows/file.yml@main
-# """
-##
diff --git a/.gitignore b/.gitignore
index 3457b7c..3bb87bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,55 +1,45 @@
-# Generated from:
-# https://github.com/plone/meta/tree/master/config/default
-# See the inline comments on how to expand/tweak this configuration file
# python related
*.egg-info
*.pyc
*.pyo
# tools related
-build/
+__pycache__/
.coverage
+.mypy_cache
+.pytest_cache
+.ruff_cache
+/build/
coverage.xml
dist/
docs/_build
-__pycache__/
-.tox
-.vscode/
node_modules/
# venv / buildout related
+.eggs/
+.installed.cfg
+.mr.developer.cfg
+.venv/
bin/
develop-eggs/
eggs/
-.eggs/
etc/
-.installed.cfg
include/
lib/
lib64
-.mr.developer.cfg
parts/
pyvenv.cfg
var/
# mxdev
-/instance/
+.installed.txt
+.lock
+/.mxdev_cache/
/.make-sentinels/
/*-mxdev.txt
+/instance/
/reports/
/sources/
/venv/
-.installed.txt
-*.mo
-
-# pipforester
-forest.dot
-forest.json
-
-##
-# Add extra configuration options in .meta.toml:
-# [gitignore]
-# extra_lines = """
-# _your own configuration lines_
-# """
-##
+constraints*.txt
+requirements*.txt
\ No newline at end of file
diff --git a/.meta.toml b/.meta.toml
deleted file mode 100644
index 770e34e..0000000
--- a/.meta.toml
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated from:
-# https://github.com/plone/meta/tree/master/config/default
-# See the inline comments on how to expand/tweak this configuration file
-[meta]
-template = "default"
-commit-id = "25d2fa7f"
-
-[pyproject]
-codespell_skip = "*.min.js"
-codespell_ignores = "vew"
-dependencies_ignores = "['zestreleaser.towncrier', 'zest.releaser', 'pytest', 'pytest-cov', 'pytest-plone']"
-dependencies_mappings = [
- "'plone.app.testing' = ['plone.testing']",
- "'Products.CMFPlone' = ['Products.CMFCore', 'Products.GenericSetup']",
- ]
-
-[tox]
-use_mxdev = true
-test_runner = "pytest"
-test_path = "/tests"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
deleted file mode 100644
index b6eb043..0000000
--- a/.pre-commit-config.yaml
+++ /dev/null
@@ -1,94 +0,0 @@
-# Generated from:
-# https://github.com/plone/meta/tree/master/config/default
-# See the inline comments on how to expand/tweak this configuration file
-ci:
- autofix_prs: false
- autoupdate_schedule: monthly
-
-repos:
-- repo: https://github.com/asottile/pyupgrade
- rev: v3.14.0
- hooks:
- - id: pyupgrade
- args: [--py38-plus]
-- repo: https://github.com/pycqa/isort
- rev: 5.12.0
- hooks:
- - id: isort
-- repo: https://github.com/psf/black
- rev: 23.9.1
- hooks:
- - id: black
-- repo: https://github.com/collective/zpretty
- rev: 3.1.0
- hooks:
- - id: zpretty
-
-##
-# Add extra configuration options in .meta.toml:
-# [pre_commit]
-# zpretty_extra_lines = """
-# _your own configuration lines_
-# """
-##
-- repo: https://github.com/PyCQA/flake8
- rev: 6.1.0
- hooks:
- - id: flake8
-
-##
-# Add extra configuration options in .meta.toml:
-# [pre_commit]
-# flake8_extra_lines = """
-# _your own configuration lines_
-# """
-##
-- repo: https://github.com/codespell-project/codespell
- rev: v2.2.6
- hooks:
- - id: codespell
- additional_dependencies:
- - tomli
-
-##
-# Add extra configuration options in .meta.toml:
-# [pre_commit]
-# codespell_extra_lines = """
-# _your own configuration lines_
-# """
-##
-- repo: https://github.com/mgedmin/check-manifest
- rev: "0.49"
- hooks:
- - id: check-manifest
-- repo: https://github.com/regebro/pyroma
- rev: "4.2"
- hooks:
- - id: pyroma
-- repo: https://github.com/mgedmin/check-python-versions
- rev: "0.21.3"
- hooks:
- - id: check-python-versions
- args: ['--only', 'setup.py,pyproject.toml']
-- repo: https://github.com/collective/i18ndude
- rev: "6.1.0"
- hooks:
- - id: i18ndude
-
-
-##
-# Add extra configuration options in .meta.toml:
-# [pre_commit]
-# i18ndude_extra_lines = """
-# _your own configuration lines_
-# """
-##
-
-
-##
-# Add extra configuration options in .meta.toml:
-# [pre_commit]
-# extra_lines = """
-# _your own configuration lines_
-# """
-##
diff --git a/CHANGES.md b/CHANGELOG.md
similarity index 100%
rename from CHANGES.md
rename to CHANGELOG.md
diff --git a/LICENSE.md b/LICENSE.md
index 7bf6429..f3545da 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,4 +1,4 @@
-kitconcept.seo Copyright 2023, Plone Community
+kitconcept.seo Copyright 2026, kitconcept GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 2
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 0595b97..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,19 +0,0 @@
-graft src/kitconcept
-graft docs
-graft news
-graft tests
-include .coveragerc
-include .dockerignore
-include .editorconfig
-include *.txt
-include *.yml
-include *.md
-exclude *-mxdev.txt
-exclude Dockerfile
-exclude mx.ini
-exclude Makefile
-recursive-exclude frontend *
-exclude instance.yaml
-global-exclude *.pyc
-global-exclude .DS_Store
-exclude *.png
diff --git a/Makefile b/Makefile
index 1d7c8a3..4155ed7 100644
--- a/Makefile
+++ b/Makefile
@@ -15,27 +15,29 @@ GREEN=`tput setaf 2`
RESET=`tput sgr0`
YELLOW=`tput setaf 3`
-PLONE6=6.0-latest
-
# Python checks
-PYTHON?=python3
+UV?=uv
# installed?
-ifeq (, $(shell which $(PYTHON) ))
- $(error "PYTHON=$(PYTHON) not found in $(PATH)")
-endif
-
-# version ok?
-PYTHON_VERSION_MIN=3.8
-PYTHON_VERSION_OK=$(shell $(PYTHON) -c "import sys; print((int(sys.version_info[0]), int(sys.version_info[1])) >= tuple(map(int, '$(PYTHON_VERSION_MIN)'.split('.'))))")
-ifeq ($(PYTHON_VERSION_OK),0)
- $(error "Need python $(PYTHON_VERSION) >= $(PYTHON_VERSION_MIN)")
+ifeq (, $(shell which $(UV) ))
+ $(error "UV=$(UV) not found in $(PATH)")
endif
BACKEND_FOLDER=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
-GIT_FOLDER=$(BACKEND_FOLDER)/.git
+ifdef PLONE_VERSION
+PLONE_VERSION := $(PLONE_VERSION)
+else
+PLONE_VERSION := 6.1.4
+endif
+
+VENV_FOLDER=$(BACKEND_FOLDER)/.venv
+export VIRTUAL_ENV=$(VENV_FOLDER)
+BIN_FOLDER=$(VENV_FOLDER)/bin
+# Environment variables to be exported
+export PYTHONWARNINGS := ignore
+export DOCKER_BUILDKIT := 1
all: build
@@ -45,68 +47,98 @@ all: build
help: ## This help message
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
-bin/pip bin/tox bin/mxdev:
- @echo "$(GREEN)==> Setup Virtual Env$(RESET)"
- $(PYTHON) -m venv .
- bin/pip install -U "pip" "wheel" "cookiecutter" "mxdev" "tox" "pre-commit"
- if [ -d $(GIT_FOLDER) ]; then bin/pre-commit install; else echo "$(RED) Not installing pre-commit$(RESET)";fi
+requirements-mxdev.txt: pyproject.toml mx.ini ## Generate constraints file
+ @echo "$(GREEN)==> Generate constraints file$(RESET)"
+ @echo '-c https://dist.plone.org/release/$(PLONE_VERSION)/constraints.txt' > requirements.txt
+ @uvx mxdev -c mx.ini
+
+$(VENV_FOLDER): requirements-mxdev.txt ## Install dependencies
+ @echo "$(GREEN)==> Install environment$(RESET)"
+ifdef CI
+ @uv venv $(VENV_FOLDER)
+else
+ @uv venv --python=3.10 $(VENV_FOLDER)
+endif
+ @uv pip install -r requirements-mxdev.txt
-.PHONY: config
-config: bin/pip ## Create instance configuration
+.PHONY: sync
+sync: $(VENV_FOLDER) ## Sync project dependencies
+ @echo "$(GREEN)==> Sync project dependencies$(RESET)"
+ @uv pip install -r requirements-mxdev.txt
+
+instance/etc/zope.ini instance/etc/zope.conf: instance.yaml ## Create instance configuration
@echo "$(GREEN)==> Create instance configuration$(RESET)"
- bin/cookiecutter -f --no-input --config-file instance.yaml gh:plone/cookiecutter-zope-instance
+ @uvx cookiecutter -f --no-input -c 2.1.1 --config-file instance.yaml gh:plone/cookiecutter-zope-instance
-.PHONY: build-dev
-build-dev: config ## pip install Plone packages
- @echo "$(GREEN)==> Setup Build$(RESET)"
- bin/mxdev -c mx.ini
- bin/pip install -r requirements-mxdev.txt
+.PHONY: config
+config: instance/etc/zope.ini
.PHONY: install
-install: build-dev ## Install Plone 6.0
-
-
-.PHONY: build
-build: build-dev ## Install Plone 6.0
+install: $(VENV_FOLDER) config ## Install Plone and dependencies
.PHONY: clean
-clean: ## Remove old virtualenv and creates a new one
+clean: ## Clean installation and instance (data left intact)
@echo "$(RED)==> Cleaning environment and build$(RESET)"
- rm -rf bin lib lib64 include share etc var inituser pyvenv.cfg .installed.cfg instance .tox .pytest_cache
+ @rm -rf $(VENV_FOLDER) pyvenv.cfg .installed.cfg instance/etc .venv .pytest_cache .ruff_cache constraints* requirements*
+
+.PHONY: remove-data
+remove-data: ## Remove all content
+ @echo "$(RED)==> Removing all content$(RESET)"
+ rm -rf $(VENV_FOLDER) instance/var
.PHONY: start
-start: ## Start a Plone instance on localhost:8080
- PYTHONWARNINGS=ignore ./bin/runwsgi instance/etc/zope.ini
+start: $(VENV_FOLDER) instance/etc/zope.ini ## Start a Plone instance on localhost:8080
+ @$(BIN_FOLDER)/runwsgi instance/etc/zope.ini
.PHONY: console
-console: ## Start a zope console
- PYTHONWARNINGS=ignore ./bin/zconsole debug instance/etc/zope.conf
+console: $(VENV_FOLDER) instance/etc/zope.ini ## Start a console into a Plone instance
+ @$(BIN_FOLDER)/zconsole debug instance/etc/zope.conf
+
+.PHONY: create-site
+create-site: $(VENV_FOLDER) instance/etc/zope.ini ## Create a new site from scratch
+ @$(BIN_FOLDER)/zconsole run instance/etc/zope.conf ./scripts/create_site.py
+
+# QA
+.PHONY: lint
+lint: ## Check and fix code base according to Plone standards
+ @echo "$(GREEN)==> Lint codebase$(RESET)"
+ @uvx ruff@latest check --fix --config $(BACKEND_FOLDER)/pyproject.toml
+ @uvx pyroma@latest -d .
+ @uvx check-python-versions@latest .
+ @uvx zpretty@latest --check src
.PHONY: format
-format: bin/tox ## Format the codebase according to our standards
+format: ## Check and fix code base according to Plone standards
@echo "$(GREEN)==> Format codebase$(RESET)"
- bin/tox -e format
+ @uvx ruff@latest check --select I --fix --config $(BACKEND_FOLDER)/pyproject.toml
+ @uvx ruff@latest format --config $(BACKEND_FOLDER)/pyproject.toml
+ @uvx zpretty@latest -i src
-.PHONY: lint
-lint: ## check code style
- bin/tox -e lint
+.PHONY: check
+check: format lint ## Check and fix code base according to Plone standards
# i18n
-bin/i18ndude: bin/pip
- @echo "$(GREEN)==> Install translation tools$(RESET)"
- bin/pip install i18ndude
-
.PHONY: i18n
-i18n: bin/i18ndude ## Update locales
+i18n: $(VENV_FOLDER) ## Update locales
@echo "$(GREEN)==> Updating locales$(RESET)"
- bin/update_dist_locale
+ @$(BIN_FOLDER)/python -m kitconcept.seo.locales
# Tests
.PHONY: test
-test: bin/tox ## run tests
- bin/tox -e test
+test: $(VENV_FOLDER) ## run tests
+ @$(BIN_FOLDER)/pytest
.PHONY: test-coverage
-test-coverage: bin/tox ## run tests with coverage
- bin/tox -e coverage
+test-coverage: $(VENV_FOLDER) ## run tests with coverage
+ @$(BIN_FOLDER)/pytest --cov=kitconcept.seo --cov-report term-missing
+
+## Add bobtemplates features (check bobtemplates.plone's documentation to get the list of available features)
+add: $(VENV_FOLDER)
+ @uvx plonecli add $(filter-out $@,$(MAKECMDGOALS))
+
+.PHONY: release
+release: $(VENV_FOLDER) ## Create a release
+ @echo "$(GREEN)==> Create a release$(RESET)"
+ @uv pip install -e ".[release]"
+ @uv run fullrelease
diff --git a/README.md b/README.md
index 0b82678..1546898 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,25 @@
# kitconcept.seo
-[](https://pypi.python.org/pypi/kitconcept.seo/)
-[](https://pypi.python.org/pypi/kitconcept.seo/)
-[](https://pypi.python.org/pypi/kitconcept.seo/)
+SEO addon for Plone
-[](https://kitconcept.com/)
+[](https://pypi.org/project/kitconcept.seo/)
+[](https://pypi.org/project/kitconcept.seo/)
+[](https://pypi.org/project/kitconcept.seo/)
+[](https://pypi.org/project/kitconcept.seo/)
+[](https://pypi.org/project/kitconcept.seo/)
+
+
+[](https://pypi.org/project/kitconcept.seo/)
+
+[](https://github.com/kitconcept/kitconcept.seo/actions/workflows/main.yml)
+
+[](https://kitconcept.com/)
[SEO](https://en.wikipedia.org/wiki/Search_engine_optimization) enhancements for the Plone Content Management System.
`kitconcept.seo` works with the latest Plone 6 and its default frontend [Volto](https://github.com/plone/volto).
It might still work with Plone Classic but that is not officially supported.
If you are looking for a full featured SEO solution for Plone Classic or older versions of Plone, we suggest looking into [fourdigits.seo](https://pypi.org/project/fourdigits.seo/).
-
## Features
- Allows to override meta title and meta description per page
@@ -19,6 +27,8 @@ If you are looking for a full featured SEO solution for Plone Classic or older v
- Allows to set a canonical URL
- Allows to set Open Graph title, description and image
+
+
## Examples
This add-on can be seen in action at the following sites:
@@ -34,9 +44,7 @@ This add-on can be seen in action at the following sites:
This product has been translated into
- German
-
- Italian
-
- Spanish
## Installation
@@ -46,12 +54,12 @@ Install kitconcept.seo with `pip`:
```shell
pip install kitconcept.seo
```
+
And to create the Plone site:
```shell
-make create_site
+make create-site
```
-
# Enable the SEO behavior
To enable the SEO tab for a specific content type you have to enable the `kitconcept.seo` behavior.
@@ -120,9 +128,36 @@ In your Volto page you need to go to the edit mode of the page you want to add a
## Contribute
-- [Issue Tracker](https://github.com/collective/kitconcept.seo/issues)
-- [Source Code](https://github.com/collective/kitconcept.seo/)
+- [Issue tracker](https://github.com/kitconcept/kitconcept.seo/issues)
+- [Source code](https://github.com/kitconcept/kitconcept.seo/)
+
+### Prerequisites ✅
+
+- An [operating system](https://6.docs.plone.org/install/create-project-cookieplone.html#prerequisites-for-installation) that runs all the requirements mentioned.
+- [uv](https://6.docs.plone.org/install/create-project-cookieplone.html#uv)
+- [Make](https://6.docs.plone.org/install/create-project-cookieplone.html#make)
+- [Git](https://6.docs.plone.org/install/create-project-cookieplone.html#git)
+- [Docker](https://docs.docker.com/get-started/get-docker/) (optional)
+
+### Installation 🔧
+
+1. Clone this repository, then change your working directory.
+
+ ```shell
+ git clone git@github.com:kitconcept/kitconcept.seo.git
+ cd kitconcept.seo
+ ```
+
+2. Install this code base.
+
+ ```shell
+ make install
+ ```
## License
The project is licensed under GPLv2.
+
+## Credits and acknowledgements 🙏
+
+Generated using [Cookieplone (0.9.10)](https://github.com/plone/cookieplone) and [cookieplone-templates (dd13073)](https://github.com/plone/cookieplone-templates/commit/dd13073d34447056d6992461d8da29447d62c029) on 2026-01-27 09:51:10.480177. A special thanks to all contributors and supporters!
diff --git a/bobtemplate.cfg b/bobtemplate.cfg
new file mode 100644
index 0000000..c7454f0
--- /dev/null
+++ b/bobtemplate.cfg
@@ -0,0 +1,7 @@
+[main]
+version = 6.1.14
+template = plone_addon
+git_init = True
+python = python3
+package.dottedname = kitconcept.seo
+package.browserlayer = BrowserLayer
diff --git a/constraints.txt b/constraints.txt
deleted file mode 100644
index 228b6cc..0000000
--- a/constraints.txt
+++ /dev/null
@@ -1 +0,0 @@
--c https://dist.plone.org/release/6.0-latest/constraints.txt
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..8c348ac
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,7 @@
+# Python related
+*.egg-info
+.venv
+
+# Documentation builds
+_build
+styles/Microsoft
diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml
new file mode 100644
index 0000000..c629b39
--- /dev/null
+++ b/docs/.readthedocs.yaml
@@ -0,0 +1,25 @@
+# .readthedocs.yaml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Set the OS, Python version and other tools you might need
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.13"
+ commands:
+ # Cancel building pull requests when there aren't changes in the docs directory or YAML file.
+ # You can add any other files or directories that you'd like here as well,
+ # like your docs requirements file, or other files that will change your docs build.
+ #
+ # If there are no changes (git diff exits with 0) we force the command to return with 183.
+ # This is a special exit code on Read the Docs that will cancel the build immediately.
+ - |
+ if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/main -- docs/docs/ docs/styles/ .readthedocs.yaml docs/.vale.ini docs/Makefile docs/uv.lock .github/workflows/docs.yml;
+ then
+ exit 183;
+ fi
+ - cd docs && make rtd-pr-preview
diff --git a/docs/.vale.ini b/docs/.vale.ini
new file mode 100644
index 0000000..f89759a
--- /dev/null
+++ b/docs/.vale.ini
@@ -0,0 +1,12 @@
+StylesPath = styles
+
+MinAlertLevel = suggestion
+
+Vocab = Base,Plone
+
+Packages = Microsoft
+
+[*]
+BasedOnStyles = Vale, Microsoft
+Microsoft.Contractions = suggestion
+Microsoft.Units = suggestion
diff --git a/docs/LICENSE.md b/docs/LICENSE.md
new file mode 100644
index 0000000..8b45b5a
--- /dev/null
+++ b/docs/LICENSE.md
@@ -0,0 +1,3 @@
+kitconcept.seo Copyright 2026, kitconcept GmbH
+
+The text and illustrations in this website are licensed by the Plone Foundation under a [Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0/) license. Plone and the Plone® logo are registered trademarks of the Plone Foundation, registered in the United States and other countries. For guidelines on the permitted uses of the Plone trademarks, see [https://plone.org/foundation/logo](https://plone.org/foundation/logo). All other trademarks are owned by their respective owners.
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..0a9dc4f
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,138 @@
+# Makefile for Sphinx documentation
+.DEFAULT_GOAL = help
+SHELL = bash
+
+# You can set these variables from the command line.
+SPHINXOPTS ?=
+PAPER ?=
+
+# Internal variables.
+GREEN=`tput setaf 2`
+RESET=`tput sgr0`
+SPHINXBUILD = "$(realpath .venv/bin/sphinx-build)"
+DOCS_DIR = ./docs/
+BUILDDIR = ./_build
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(DOCS_DIR)
+VALEFILES := $(shell find $(DOCS_DIR) -type f -name "*.md" -print)
+VALEOPTS ?=
+PYTHONVERSION = >=3.11,<3.14
+
+# Add the following 'help' target to your Makefile
+# And add help text after each target name starting with '\#\#'
+.PHONY: help
+help: # This help message
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+
+
+# environment management
+.venv/bin/python: ## Create Python virtual environment and install package requirements
+ @uv sync
+
+.PHONY: install
+install: .venv/bin/python ## Sync package requirements
+
+
+.PHONY: init
+init: clean clean-python .venv/bin/python ## Clean docs build directory and Python, and initialize Python virtual environment
+
+.PHONY: clean
+clean: ## Clean docs build directory
+ cd $(DOCS_DIR) && rm -rf $(BUILDDIR)/
+
+.PHONY: clean-python
+clean-python: clean
+ rm -rf .venv/
+ rm -rf *.egg-info
+# /environment management
+
+
+# documentation builders
+.PHONY: html
+html: .venv/bin/python ## Build html
+ @uv run sphinx-build -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+.PHONY: livehtml
+livehtml: .venv/bin/python ## Rebuild Sphinx documentation on changes, with live-reload in the browser
+ @uv run sphinx-autobuild \
+ --ignore "*.swp" \
+ --port 8050 \
+ -b html $(DOCS_DIR) "$(BUILDDIR)/html" $(SPHINXOPTS) $(O)
+
+.PHONY: dirhtml
+dirhtml: .venv/bin/python
+ @uv run sphinx-build -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+.PHONY: singlehtml
+singlehtml: .venv/bin/python
+ @uv run sphinx-build -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+.PHONY: text
+text: .venv/bin/python
+ @uv run sphinx-build -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+.PHONY: changes
+changes: .venv/bin/python
+ @uv run sphinx-build -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+.PHONY: rtd-prepare
+rtd-prepare: ## Prepare environment on Read the Docs
+ asdf plugin add uv
+ asdf install uv latest
+ asdf global uv latest
+
+.PHONY: rtd-pr-preview ## Build pull request previews on Read the Docs
+rtd-pr-preview: rtd-prepare .venv/bin/python ## Build pull request preview on Read the Docs
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) ${READTHEDOCS_OUTPUT}/html/
+
+.PHONY: build
+build: html ## Build documentation in HTML format
+
+# /documentation builders
+
+
+# test
+.PHONY: linkcheck
+linkcheck: .venv/bin/python ## Run linkcheck
+ @uv run sphinx-build -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/ ."
+
+.PHONY: linkcheckbroken
+linkcheckbroken: .venv/bin/python ## Run linkcheck and show only broken links
+ @uv run sphinx-build -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck | GREP_COLORS='0;31' grep -wi "broken\|redirect" --color=always | GREP_COLORS='0;31' grep -vi "https://github.com/plone/volto/issues/" --color=always && if test $$? = 0; then exit 1; fi || test $$? = 1
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/ ."
+
+.PHONY: doctest
+doctest: .venv/bin/python ## Test snippets in the documentation
+ @uv run sphinx-build -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+.PHONY: vale
+vale: .venv/bin/python ## Run Vale style, grammar, and spell checks
+ @uv run vale sync
+ @uv run vale --no-wrap $(VALEOPTS) $(VALEFILES)
+ @echo "Vale is finished; look for any errors in the above output."
+ @echo
+
+.PHONY: test
+test: clean vale linkcheckbroken doctest ## Clean docs build, then run vale, linkcheckbroken, and doctest
+
+.PHONY: all
+all: clean vale linkcheckbroken html ## Clean docs build, then run linkcheckbroken, and build html
+# /test
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..f89503e
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,124 @@
+# kitconcept.seo
+
+Documentation for kitconcept.seo.
+SEO addon for Plone
+
+This project provides a Sphinx-based documentation environment for your Plone project, powered by the [Plone Sphinx Theme](https://github.com/plone/plone-sphinx-theme).
+It's generated using the `documentation_starter` template from [Cookieplone](https://github.com/plone/cookieplone).
+
+
+## Prerequisites
+
+- [uv](https://docs.astral.sh/uv/) is the recommended tool for managing Python versions and project dependencies.
+
+To install uv, use the following command, or visit the [uv installation page](https://docs.astral.sh/uv/getting-started/installation/) for alternative methods.
+
+```shell
+curl -LsSf https://astral.sh/uv/install.sh | sh
+```
+
+
+## Build documentation
+
+To build the HTML documentation, issue the following command.
+
+```shell
+make html
+```
+
+To build the HTML documentation and view a live preview while editing your documentation, issue the following command.
+
+```shell
+make livehtml
+```
+
+To check for broken links in your documentation, issue the following command.
+
+```shell
+make linkcheckbroken
+```
+
+To check spelling, grammar, and style in your documentation, issue the following command.
+You should pay attention to errors and warnings, and suggestions may get noisy.
+
+```shell
+make vale
+```
+
+To delete the `docs` build directory and Python virtual environment, and reinitialize Python virtual environment, issue the following command.
+This is useful to force reinstall dependencies and purge cached files in Sphinx builds.
+
+```shell
+make init
+```
+
+For more `make` commands, issue the following command.
+
+```shell
+make help
+```
+
+
+## Customize the kitconcept.seo documentation
+
+This section provides how-to guidance to customize your documentation.
+
+The file `docs/conf.py` controls the configuration of your documentation.
+It has extensive comments for each part, often with links to the authoritative documentation for extensions and configuration.
+
+The following sections describe customization not addressed in `docs/conf.py`.
+
+
+### Manage dependencies
+
+You can configure which dependencies or requirements you want to use in your documentation using uv.
+Requirements are stored in the `dev` dependency group in the `pyproject.toml` file.
+
+To add a requirement, use the following command.
+
+```shell
+uv add --dev my-requirement
+```
+
+To remove a requirement, use the following command.
+
+```shell
+uv remove --dev my-requirement
+```
+
+See also uv's documentation [Development dependencies](https://docs.astral.sh/uv/concepts/projects/dependencies/#development-dependencies).
+
+After installing a dependency, you might need to add it to your documentation's configuration file, `conf.py`, under the `extensions` key.
+
+
+### Replace static files
+
+You might want to replace the default static files, located in `docs/_static`, `logo.svg` and `favicon.ico`.
+Plone Sphinx Theme is configured to use these files when rendering documentation to HTML.
+
+If you rename `logo.svg`, you must update `conf.py`, under the `html_logo`, `ogp_image`, and `latex_logo` keys.
+
+
+## Read the Docs
+
+Now that you've built your documentation, how do you publish it?
+And how do you get collaborators to easily review your changes to your documentation?
+
+Thankfully, [Read the Docs](https://about.readthedocs.com/) provides both hosting of documentation and pull request previews.
+For public repositories, this service is free.
+However, the Plone Foundation donates to Read the Docs for their unwavering support to open source software, and encourages you to do the same.
+
+Your documentation scaffold is partially configured to use Read the Docs.
+In several files, search for the string `MY_READTHEDOCS_PROJECT_SLUG`.
+You'll need to replace that string with the slug that Read the Docs creates for you when you import your project.
+
+For complete documentation of this process, see the Plone 6 Documentation [Pull request preview builds](https://6.docs.plone.org/contributing/documentation/admins.html#pull-request-preview-builds).
+
+See also Read the Docs documentation:
+
+- [Pull request previews](https://docs.readthedocs.com/platform/stable/pull-requests.html)
+- [Build process overview](https://docs.readthedocs.com/platform/stable/builds.html)
+
+## Credits and acknowledgements 🙏
+
+This was generated by the [cookieplone-templates documentation_starter template](https://github.com/plone/cookieplone-templates/tree/main/documentation_starter) on 2026-01-27 12:52:31. A special thanks to all contributors and supporters!
diff --git a/docs/docs/_static/favicon.ico b/docs/docs/_static/favicon.ico
new file mode 100644
index 0000000..4904846
Binary files /dev/null and b/docs/docs/_static/favicon.ico differ
diff --git a/docs/docs/_static/logo.svg b/docs/docs/_static/logo.svg
new file mode 100644
index 0000000..b597336
--- /dev/null
+++ b/docs/docs/_static/logo.svg
@@ -0,0 +1,54 @@
+
+
+
diff --git a/docs/docs/_templates/404.html b/docs/docs/_templates/404.html
new file mode 100644
index 0000000..782c30d
--- /dev/null
+++ b/docs/docs/_templates/404.html
@@ -0,0 +1,20 @@
+{% extends "!page.html" %}
+
+{%- block htmltitle %}
+
{{ title|striptags|e }}{%- for parent in parents %} – {{ parent.title }}{%- endfor %}{{ titlesuffix }}
+{%- endblock %}
+
+{% block body %}
+
+
Page not found
+
We could not find the page you requested.
+
You can search.
+
+
+
+{% endblock %}
diff --git a/docs/docs/concepts/index.md b/docs/docs/concepts/index.md
new file mode 100644
index 0000000..1797fba
--- /dev/null
+++ b/docs/docs/concepts/index.md
@@ -0,0 +1,20 @@
+---
+myst:
+ html_meta:
+ "description": "kitconcept.seo concepts"
+ "property=og:description": "kitconcept.seo concepts"
+ "property=og:title": "kitconcept.seo concepts"
+ "keywords": "Plone, kitconcept.seo, concepts"
+---
+
+# Concepts
+
+This part of the documentation contains conceptual guides, including design defense and explanation of concepts for deeper study.
+The Diátaxis framework also calls this class of documentation _explanation_.
+
+> Explanation is a discursive treatment of a subject, that permits reflection.
+> Explanation is understanding-oriented.
+
+```{seealso}
+https://diataxis.fr/explanation/
+```
diff --git a/docs/docs/conf.py b/docs/docs/conf.py
new file mode 100644
index 0000000..4c3f0db
--- /dev/null
+++ b/docs/docs/conf.py
@@ -0,0 +1,367 @@
+# Configuration file for the Sphinx documentation builder.
+# kitconcept.seo build configuration file
+
+
+# -- Path setup --------------------------------------------------------------
+
+from datetime import datetime
+from packaging.version import Version
+from plone_sphinx_theme import __version__
+
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath("../src/plone_sphinx_theme"))
+
+# -- Project information -----------------------------------------------------
+
+project = "kitconcept.seo"
+author = "kitconcept GmbH"
+trademark_name = "kitconcept"
+now = datetime.now()
+year = str(now.year)
+
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The full version, including alpha/beta/rc tags.
+release = __version__
+# The short X.Y version.
+version = "v" + (Version(str(release)).base_version)
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+# language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+# today = ""
+# Else, today_fmt is used as the format for a strftime call.
+# today_fmt = "%B %d, %Y"
+
+
+# -- General configuration ----------------------------------------------------
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# Add any Sphinx extension module names here, as strings.
+# They can be extensions coming with Sphinx (named "sphinx.ext.*")
+# or your custom ones.
+extensions = [
+ "myst_parser",
+ "notfound.extension",
+ "sphinx.ext.autodoc",
+ "sphinx.ext.autosummary", # plone.api
+ "sphinx.ext.doctest", # plone.api
+ "sphinx.ext.graphviz",
+ "sphinx.ext.ifconfig",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.todo",
+ "sphinx.ext.viewcode", # plone.api
+ "sphinx_copybutton",
+ "sphinx_design",
+ "sphinx_examples",
+ "sphinx_reredirects",
+ "sphinx_sitemap",
+ "sphinx_tippy",
+ "sphinxcontrib.httpdomain", # plone.restapi
+ "sphinxcontrib.httpexample", # plone.restapi
+ "sphinxcontrib.mermaid",
+ "sphinxcontrib.video",
+ "sphinxcontrib.youtube",
+ "sphinxext.opengraph",
+]
+
+# If true, the Docutils Smart Quotes transform, originally based on SmartyPants
+# (limited to English) and currently applying to many languages, will be used
+# to convert quotes and dashes to typographically correct entities.
+smartquotes = False
+
+# Options for the linkcheck builder
+# Ignore localhost
+linkcheck_ignore = [
+ # Ignore local and example URLs
+ r"http://0.0.0.0",
+ r"http://127.0.0.1",
+ r"http://localhost",
+ # Ignore file downloads
+ r"^/_static/",
+ # Ignore pages that require authentication
+ r"https://github.com/kitconcept/kitconceptseo/issues/new", # requires auth
+ # Ignore github.com pages with anchors
+ r"https://github.com/.*#.*",
+ # Ignore other specific anchors
+]
+linkcheck_allowed_redirects = { # TODO: Confirm usage of linkcheck_allowed_redirects
+ # All HTTP redirections from the source URI to the canonical URI will be treated
+ # as "working".
+}
+linkcheck_anchors = True
+linkcheck_timeout = 5
+linkcheck_retries = 1
+
+# The suffix of source filenames.
+source_suffix = {
+ ".md": "markdown",
+ ".rst": "restructuredtext",
+}
+
+# The master toctree document.
+master_doc = "index"
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+suppress_warnings = []
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = "plone_sphinx_theme" # This can be configured
+html_logo = "_static/logo.svg"
+html_favicon = "_static/favicon.ico"
+# The default value includes icon-links, so override it with that one omitted,
+# and add it to html_theme_options[footer_content_items].
+html_sidebars = {
+ "**": [
+ "navbar-logo",
+ "search-button-field",
+ "sbt-sidebar-nav",
+ ]
+}
+html_theme_options = {
+ "article_header_start": ["toggle-primary-sidebar"],
+ "footer_content_items": [
+ "author",
+ "copyright",
+ "last-updated",
+ "extra-footer",
+ "icon-links",
+ ],
+ # Uncomment for a page-wide footer
+ # "footer_end": ["version.html"],
+ "icon_links": [
+ {
+ "name": "GitHub",
+ "url": "https://github.com/kitconcept/kitconceptseo",
+ "icon": "fa-brands fa-square-github",
+ "type": "fontawesome",
+ "attributes": {
+ "target": "_blank",
+ "rel": "noopener me",
+ "class": "nav-link custom-fancy-css",
+ },
+ },
+ # {
+ # "name": "Mastodon",
+ # "url": "https://MY_MASTODON_SERVER/@MY_MASTODON_USER",
+ # "icon": "fa-brands fa-mastodon",
+ # "type": "fontawesome",
+ # "attributes": {
+ # "target": "_blank",
+ # "rel": "noopener me",
+ # "class": "nav-link custom-fancy-css",
+ # },
+ # },
+ ],
+ "logo": {
+ "text": "kitconcept.seo",
+ },
+ "navigation_with_keys": True,
+ "path_to_docs": "docs/docs",
+ "repository_branch": "main",
+ "repository_url": "https://github.com/kitconcept/kitconceptseo",
+ "search_bar_text": "Search",
+ "show_toc_level": 2,
+ "use_edit_page_button": True,
+ "use_issues_button": True,
+ "use_repository_button": True,
+}
+# suggest edit link
+# remark: is mandatory in "edit_page_url_template"
+# html_context = {
+# "edit_page_url_template": "https://github.com/kitconcept/kitconceptseo/edit/main/docs/",
+# }
+
+# Announce that we have an opensearch plugin
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_use_opensearch
+html_use_opensearch = "https://MY_READTHEDOCS_PROJECT_SLUG.readthedocs.io"
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# " v documentation".
+html_title = f"{project} v{release}"
+
+# If false, no index is generated.
+html_use_index = True
+
+# html_css_files = ["custom.css", ("print.css", {"media": "print"})]
+# html_js_files = []
+
+html_extra_path = [
+ "robots.txt",
+]
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = [
+ "_static",
+]
+
+
+# -- Options for autodoc ----------------------------------------------------
+
+# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration
+# Automatically extract typehints when specified and place them in
+# descriptions of the relevant function/method.
+# autodoc_typehints = "description"
+
+# Don't show class signature with the class' name.
+autodoc_class_signature = "separated"
+
+
+# -- Options for sphinx_sitemap to html -----------------------------
+
+# Used by sphinx_sitemap to generate a sitemap
+html_baseurl = "https://MY_READTHEDOCS_PROJECT_SLUG.readthedocs.io/"
+# https://sphinx-sitemap.readthedocs.io/en/latest/advanced-configuration.html#customizing-the-url-scheme
+sitemap_url_scheme = "{link}"
+sitemap_filename = "sitemap-custom.xml"
+
+# -- Options for myST markdown conversion to html -----------------------------
+
+# For more information see:
+# https://myst-parser.readthedocs.io/en/latest/syntax/optional.html
+myst_enable_extensions = [
+ "attrs_block", # Support parsing of block attributes.
+ "attrs_inline", # Support parsing of inline attributes.
+ "colon_fence", # Use ::: delimiters to denote code fences, instead of ```
+ "deflist", # Support definition lists. https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#definition-lists
+ "html_image", # For inline images. See https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#html-images
+ "linkify", # Identify "bare" web URLs and add hyperlinks.
+ "strikethrough", # See https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#syntax-strikethrough
+ "substitution", # Use Jinja2 for substitutions. https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#substitutions-with-jinja2
+]
+
+myst_substitutions = {}
+
+# -- Intersphinx configuration ----------------------------------
+
+# This extension can generate automatic links to the documentation of objects
+# in other projects. Usage is simple: whenever Sphinx encounters a
+# cross-reference that has no matching target in the current documentation set,
+# it looks for targets in the documentation sets configured in
+# intersphinx_mapping. A reference like :py:class:`zipfile.ZipFile` can then
+# linkto the Python documentation for the ZipFile class, without you having to
+# specify where it is located exactly.
+#
+# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html
+intersphinx_mapping = {
+ "python": ("https://docs.python.org/3/", None),
+ "plone": ("https://6.docs.plone.org/", None),
+}
+
+
+# -- GraphViz configuration ----------------------------------
+graphviz_output_format = "svg"
+
+
+# -- Mermaid configuration ----------------------------------
+mermaid_version = "11.2.0"
+
+
+# -- OpenGraph configuration ----------------------------------
+ogp_site_url = "https://MY_READTHEDOCS_PROJECT_SLUG.readthedocs.io/"
+ogp_description_length = 200
+ogp_image = "https://MY_READTHEDOCS_PROJECT_SLUG/_static/MY_LOGO.svg"
+ogp_site_name = "kitconcept.seo Documentation"
+ogp_type = "website"
+ogp_custom_meta_tags = [
+ '',
+]
+
+
+# -- sphinx.ext.todo -----------------------
+# See http://sphinx-doc.org/ext/todo.html#confval-todo_include_todos
+todo_include_todos = True
+
+
+# -- sphinx-notfound-page configuration ----------------------------------
+notfound_urls_prefix = ""
+notfound_template = "404.html"
+
+
+# -- sphinx-reredirects configuration ----------------------------------
+# https://documatt.com/sphinx-reredirects/usage.html
+redirects = {}
+
+
+# -- sphinx-tippy configuration ----------------------------------
+tippy_anchor_parent_selector = "article.bd-article"
+tippy_enable_doitips = False
+tippy_enable_wikitips = False
+tippy_props = {
+ "interactive": True,
+ "placement": "auto-end",
+}
+
+
+# -- Options for HTML help output -------------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = "kitconcept.seoDocumentation"
+
+
+# -- Options for LaTeX output -------------------------------------------------
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual])
+latex_documents = [
+ (
+ "index",
+ "kitconcept.seoDocumentation.tex",
+ "kitconcept.seo Documentation",
+ "kitconcept community",
+ "manual",
+ ),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+latex_logo = "_static/logo.svg"
+
+
+# -- Configuration for source_replacements extension -----------------------
+# An extension that allows replacements for code blocks that
+# are not supported in `rst_epilog` or other substitutions.
+# https://stackoverflow.com/a/56328457/2214933
+def source_replace(app, docname, source):
+ result = source[0]
+ for key in app.config.source_replacements:
+ result = result.replace(key, app.config.source_replacements[key])
+ source[0] = result
+
+
+# Dict of replacements.
+source_replacements = {
+ "{SUPPORTED_PYTHON_VERSIONS}": "3.10, 3.11, 3.12, or 3.13",
+}
+
+
+# Finally, configure app attributes.
+def setup(app):
+ app.add_config_value("source_replacements", {}, True)
+ app.connect("source-read", source_replace)
+ # For sphinx.ext.ifconfig
+ app.add_config_value("context", "documentation", "env")
diff --git a/docs/docs/glossary.md b/docs/docs/glossary.md
new file mode 100644
index 0000000..ee89125
--- /dev/null
+++ b/docs/docs/glossary.md
@@ -0,0 +1,57 @@
+---
+myst:
+ html_meta:
+ "description": "Terms and definitions used throughout the Plone Sphinx Theme documentation."
+ "property=og:description": "Terms and definitions used throughout the Plone Sphinx Theme documentation."
+ "property=og:title": "Glossary"
+ "keywords": "Plone, documentation, glossary, term, definition"
+---
+
+This glossary provides example terms and definitions relevant to **kitconcept.seo**.
+SEO addon for Plone
+
+```{note}
+This is an example glossary demonstrating MyST Markdown’s `{glossary}` directive. You can adapt it for your project’s appendix by editing or replacing these entries with your own terms and definitions.
+```
+
+(glossary-label)=
+
+# Glossary
+
+```{glossary}
+:sorted: true
+
+Plone
+ [Plone](https://plone.org/) is an open-source content management system that is used to create, edit, and manage digital content, like websites, intranets and custom solutions.
+ It comes with over 20 years of growth, optimisations, and refinements.
+ The result is a system trusted by governments, universities, businesses, and other organisations all over the world.
+
+add-on
+ An add-on in Plone extends its functionality.
+ It is code that is released as a package to make it easier to install.
+
+ In Volto, an add-on is a JavaScript package.
+
+ In Plone core, an add-on is a Python package.
+
+ - [Plone core add-ons](https://github.com/collective/awesome-plone#readme)
+ - [Volto add-ons](https://github.com/collective/awesome-volto#readme)
+ - [Add-ons tagged with the trove classifier `Framework :: Plone` on PyPI](https://pypi.org/search/?c=Framework+%3A%3A+Plone)
+
+Plone Sphinx Theme
+plone-sphinx-theme
+ [Plone Sphinx Theme](https://plone-sphinx-theme.readthedocs.io/) is a Sphinx theme for [Plone 6 Documentation](https://6.docs.plone.org/), [Plone Conference Training](https://training.plone.org/), and documentation of various Plone packages.
+ This scaffold uses Plone Sphinx Theme.
+
+Markedly Structured Text
+MyST
+ [Markedly Structured Text (MyST)](https://myst-parser.readthedocs.io/en/latest/) is a rich and extensible flavor of Markdown, for authoring Plone Documentation.
+ The sample documentation in this scaffold is written in MyST.
+
+Sphinx
+ [Sphinx](https://www.sphinx-doc.org/en/master/) is a tool that makes it easy to create intelligent and beautiful documentation.
+ It was originally created for Python documentation, and it has excellent facilities for the documentation of software projects in a range of languages.
+ It can generate multiple output formats, including HTML and PDF, from a single source.
+ This scaffold uses Sphinx to generate documentation in HTML format.
+
+```
diff --git a/docs/docs/how-to-guides/index.md b/docs/docs/how-to-guides/index.md
new file mode 100644
index 0000000..fb99ac5
--- /dev/null
+++ b/docs/docs/how-to-guides/index.md
@@ -0,0 +1,30 @@
+---
+myst:
+ html_meta:
+ "description": "kitconcept.seo how-to guides"
+ "property=og:description": "kitconcept.seo how-to guides"
+ "property=og:title": "kitconcept.seo how-to guides"
+ "keywords": "Plone, kitconcept.seo, how-to, guides"
+---
+
+# How-to guides
+
+This part of the documentation contains how-to guides, including installation and usage.
+
+> How-to guides are directions that guide the reader through a problem or towards a result.
+> How-to guides are goal-oriented.
+
+```{seealso}
+https://diataxis.fr/how-to-guides/
+```
+
+
+## Authors
+
+- {doc}`plone:contributing/documentation/myst-reference`
+- {doc}`plone:contributing/documentation/authors`
+
+
+## Designers
+
+- [Contribute to Plone Sphinx Theme](https://plone-sphinx-theme.readthedocs.io/guides/contribute.html)
diff --git a/docs/docs/index.md b/docs/docs/index.md
new file mode 100644
index 0000000..e428b70
--- /dev/null
+++ b/docs/docs/index.md
@@ -0,0 +1,60 @@
+---
+myst:
+ html_meta:
+ "description": "SEO addon for Plone"
+ "property=og:description": "SEO addon for Plone"
+ "property=og:title": "kitconcept.seo"
+ "keywords": "kitconcept.seo, documentation, SEO addon for Plone"
+---
+
+# kitconcept.seo
+
+Welcome to the documentation for kitconcept.seo!
+SEO addon for Plone
+
+This scaffold provides a ready-to-use environment for creating comprehensive documentation for {term}`Plone` projects, based on {term}`Plone Sphinx Theme`.
+
+Built with Markedly Structured Text ({term}`MyST`), this environment supports rich formatting, directives, and extensions tailored for technical documentation.
+
+It's structured following the [Diátaxis](https://diataxis.fr/) documentation framework.
+
+```{toctree}
+:caption: How to guides
+:maxdepth: 2
+:hidden: true
+
+how-to-guides/index
+```
+
+```{toctree}
+:caption: Reference
+:maxdepth: 2
+:hidden: true
+
+reference/index
+```
+
+```{toctree}
+:caption: Tutorials
+:maxdepth: 2
+:hidden: true
+
+tutorials/index
+```
+
+```{toctree}
+:caption: Concepts
+:maxdepth: 2
+:hidden: true
+
+concepts/index
+```
+
+```{toctree}
+:caption: Appendices
+:maxdepth: 2
+:hidden: true
+
+glossary
+genindex
+```
diff --git a/docs/docs/reference/index.md b/docs/docs/reference/index.md
new file mode 100644
index 0000000..e710616
--- /dev/null
+++ b/docs/docs/reference/index.md
@@ -0,0 +1,23 @@
+---
+myst:
+ html_meta:
+ "description": "kitconcept.seo Reference"
+ "property=og:description": "kitconcept.seo Reference"
+ "property=og:title": "kitconcept.seo Reference"
+ "keywords": "Plone, _kitconcept.seo,_ reference"
+---
+
+# Reference
+
+This part of the documentation contains reference material, including APIs, configuration values, and environment variables.
+
+> Reference guides are technical descriptions of the machinery and how to operate it.
+> Reference material is information-oriented.
+
+```{seealso}
+https://diataxis.fr/reference/
+```
+
+## Configuration
+
+- {doc}`plone:contributing/documentation/themes-and-extensions`
diff --git a/docs/docs/robots.txt b/docs/docs/robots.txt
new file mode 100644
index 0000000..b68dd24
--- /dev/null
+++ b/docs/docs/robots.txt
@@ -0,0 +1,8 @@
+# Disallow all user agents from the following directories and files
+User-agent: *
+Disallow: /_sources/
+Disallow: /.doctrees/
+Disallow: /*.txt$
+Disallow: /.buildinfo$
+Disallow: /objects.inv$
+Sitemap: https://MY_READTHEDOCS_PROJECT_SLUG.readthedocs.io/sitemap-custom.xml
diff --git a/docs/docs/tutorials/index.md b/docs/docs/tutorials/index.md
new file mode 100644
index 0000000..5725e8b
--- /dev/null
+++ b/docs/docs/tutorials/index.md
@@ -0,0 +1,19 @@
+---
+myst:
+ html_meta:
+ "description": "kitconcept.seo Tutorials"
+ "property=og:description": "kitconcept.seo Tutorials"
+ "property=og:title": "kitconcept.seo Tutorials"
+ "keywords": "Plone, kitconcept.seo, tutorials"
+---
+
+# Tutorials
+
+This part of the documentation contains tutorials.
+
+> A tutorial is an experience that takes place under the guidance of a tutor.
+> A tutorial is always learning-oriented.
+
+```{seealso}
+https://diataxis.fr/tutorials/
+```
diff --git a/docs/pyproject.toml b/docs/pyproject.toml
new file mode 100644
index 0000000..fd7dc75
--- /dev/null
+++ b/docs/pyproject.toml
@@ -0,0 +1,61 @@
+[project]
+name = "kitconcept_seo_Documentation"
+description = "SEO addon for Plone"
+keywords = [
+ "kitconcept.seo",
+ "documentation",
+ "Plone",
+ "Diátaxis",
+]
+version="1.0.0"
+readme = "README.md"
+requires-python = ">=3.10,<3.14"
+license = { file = "LICENSE" }
+maintainers = [
+ { name = "kitconcept GmbH", email = "info@kitconcept.com" },
+]
+authors = [
+ { name = "kitconcept GmbH", email = "info@kitconcept.com" },
+]
+classifiers = [
+ "Development Status :: 1 - Planning",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+]
+
+[dependency-groups]
+dev = [
+ "graphviz",
+ "linkify-it-py",
+ "myst-parser",
+ "plone-sphinx-theme>=1.4.1", # Use the latest stable version or a minimum
+ "setuptools", # required by sphinxcontrib.httpexample, remove when it migrates to importlib. See https://github.com/collective/sphinxcontrib-httpexample/issues/106
+ "sphinx-autobuild",
+ "sphinx-copybutton",
+ "sphinx-design",
+ "sphinx-examples",
+ "sphinx-notfound-page",
+ "sphinx-reredirects",
+ "sphinx-sitemap",
+ "sphinx-tippy",
+ "sphinxcontrib-httpdomain",
+ "sphinxcontrib-httpexample",
+ "sphinxcontrib-mermaid",
+ "sphinxcontrib-video",
+ "sphinxcontrib-youtube",
+ "sphinxext-opengraph",
+ "vale",
+]
+
+[project.urls]
+Repository = "https://github.com/kitconcept/kitconceptseo"
+Documentation = "https://MY_READTHEDOCS_PROJECT_SLUG.readthedocs.io/"
+
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
diff --git a/docs/.gitkeep b/docs/styles/config/vocabularies/Base/accept.txt
similarity index 100%
rename from docs/.gitkeep
rename to docs/styles/config/vocabularies/Base/accept.txt
diff --git a/docs/styles/config/vocabularies/Base/reject.txt b/docs/styles/config/vocabularies/Base/reject.txt
new file mode 100644
index 0000000..e69de29
diff --git a/docs/styles/config/vocabularies/Plone/accept.txt b/docs/styles/config/vocabularies/Plone/accept.txt
new file mode 100644
index 0000000..cde1ced
--- /dev/null
+++ b/docs/styles/config/vocabularies/Plone/accept.txt
@@ -0,0 +1,51 @@
+`plone.api`
+`plone.restapi`
+`plone.volto`
+accessor
+APIs
+[Aa]sync
+[Bb]ackend
+backport(ed|ing)
+Barceloneta
+[Bb]oolean
+bugfix
+buildout
+cacheable
+CommonJS
+doctest
+folderish
+fieldset
+getter
+JavaScript
+[Jj]enkins
+jQuery
+libxslt
+Mockup
+npm
+nvm
+Pastanaga
+PLIP(s)
+Plone
+pluggab(le|ility)
+[Pp]ortlet
+prerendered
+programatically
+[Qq]uerystring
+Razzle
+[Rr]enderer
+RichText
+Sass
+Schuko
+subfolder
+[Tt]owncrier
+transpile[dr]{0,1}
+[Uu]ncomment
+[Uu]nhide
+unregister
+UUID
+uv
+validator
+[Vv]iewlet
+Volto
+Vue
+Zope
diff --git a/docs/styles/config/vocabularies/Plone/reject.txt b/docs/styles/config/vocabularies/Plone/reject.txt
new file mode 100644
index 0000000..81510d5
--- /dev/null
+++ b/docs/styles/config/vocabularies/Plone/reject.txt
@@ -0,0 +1,3 @@
+[^.]js
+NodeJS
+[Pp]re-requisite
diff --git a/instance.yaml b/instance.yaml
index 1d5501b..73e0f3e 100644
--- a/instance.yaml
+++ b/instance.yaml
@@ -1,8 +1,5 @@
+# Configuration for https://github.com/plone/cookiecutter-zope-instance
+
default_context:
- initial_user_name: 'admin'
initial_user_password: 'admin'
-
- load_zcml:
- package_includes: ['kitconcept.seo']
-
- db_storage: direct
+ zcml_package_includes: 'kitconcept.seo'
\ No newline at end of file
diff --git a/mx.ini b/mx.ini
index 3629ad6..70f2001 100644
--- a/mx.ini
+++ b/mx.ini
@@ -4,6 +4,7 @@
; to learn more about mxdev visit https://pypi.org/project/mxdev/
[settings]
+main-package = -e .[test]
; example how to override a package version
; version-overrides =
; example.package==2.1.0a2
@@ -13,4 +14,4 @@
; url = https://github.com/collective/example.contenttype.git
; pushurl = git@github.com:collective/example.contenttype.git
; extras = test
-; branch = feature-7
+; branch = master
diff --git a/news/+changelog.internal b/news/+changelog.internal
new file mode 100644
index 0000000..3000624
--- /dev/null
+++ b/news/+changelog.internal
@@ -0,0 +1 @@
+Add GHA checking for changelog entry on pull requests. @ericof
diff --git a/news/+plone6.1.feature b/news/+plone6.1.feature
new file mode 100644
index 0000000..18e876d
--- /dev/null
+++ b/news/+plone6.1.feature
@@ -0,0 +1 @@
+Add support for Plone 6.1. @ericof
diff --git a/news/+plone6.2.feature b/news/+plone6.2.feature
new file mode 100644
index 0000000..8019a00
--- /dev/null
+++ b/news/+plone6.2.feature
@@ -0,0 +1 @@
+Add support for Plone 6.2. @ericof
diff --git a/news/+py312.feature b/news/+py312.feature
new file mode 100644
index 0000000..1bc2f53
--- /dev/null
+++ b/news/+py312.feature
@@ -0,0 +1 @@
+Add support for Python 3.12. @ericof
diff --git a/news/+py313.feature b/news/+py313.feature
new file mode 100644
index 0000000..0a29f32
--- /dev/null
+++ b/news/+py313.feature
@@ -0,0 +1 @@
+Add support for Python 3.13. @ericof
diff --git a/news/+py38.breaking b/news/+py38.breaking
new file mode 100644
index 0000000..b7d72b6
--- /dev/null
+++ b/news/+py38.breaking
@@ -0,0 +1 @@
+Drop support for Python 3.8. @ericof
diff --git a/news/+py39.breaking b/news/+py39.breaking
new file mode 100644
index 0000000..0af81ff
--- /dev/null
+++ b/news/+py39.breaking
@@ -0,0 +1 @@
+Drop support for Python 3.9. @ericof
diff --git a/news/.changelog_template.jinja b/news/.changelog_template.jinja
new file mode 100644
index 0000000..0cf429a
--- /dev/null
+++ b/news/.changelog_template.jinja
@@ -0,0 +1,15 @@
+{% if sections[""] %}
+{% for category, val in definitions.items() if category in sections[""] %}
+
+### {{ definitions[category]['name'] }}
+
+{% for text, values in sections[""][category].items() %}
+- {{ text }} {{ values|join(', ') }}
+{% endfor %}
+
+{% endfor %}
+{% else %}
+No significant changes.
+
+
+{% endif %}
diff --git a/news/2.internal b/news/2.internal
new file mode 100644
index 0000000..a286fbf
--- /dev/null
+++ b/news/2.internal
@@ -0,0 +1 @@
+Update codebase to the latest standard for Plone backend addons. @ericof
diff --git a/news/2.tests b/news/2.tests
new file mode 100644
index 0000000..816c319
--- /dev/null
+++ b/news/2.tests
@@ -0,0 +1 @@
+Increase test coverage for the package. @ericof
diff --git a/pyproject.toml b/pyproject.toml
index 48d7352..a136707 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,11 +1,93 @@
-# Generated from:
-# https://github.com/plone/meta/tree/master/config/default
-# See the inline comments on how to expand/tweak this configuration file
+[project]
+name = "kitconcept.seo"
+dynamic = ["version"]
+description = "SEO addon for Plone"
+readme = "README.md"
+license = "GPL-2.0-only"
+requires-python = ">=3.10"
+authors = [
+ { name = "kitconcept GmbH", email = "info@kitconcept.com" },
+]
+keywords = [
+ "CMS",
+ "Plone",
+ "Python",
+ "SEO",
+]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Environment :: Web Environment",
+ "Framework :: Plone",
+ "Framework :: Plone :: 6.0",
+ "Framework :: Plone :: 6.1",
+ "Framework :: Plone :: 6.2",
+ "Framework :: Plone :: Addon",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+]
+dependencies = [
+ "Products.CMFPlone",
+ "plone.api",
+ "plone.restapi",
+]
+
+[project.optional-dependencies]
+test = [
+ "horse-with-no-namespace",
+ "plone.app.testing",
+ "plone.restapi[test]",
+ "pytest",
+ "pytest-cov",
+ "pytest-plone>=1.0.0a2",
+]
+release = [
+ "zest.releaser[recommended]",
+ "zestreleaser.towncrier",
+ "zest.pocompile",
+]
+
+[project.urls]
+Homepage = "https://github.com/kitconcept/kitconcept.seo"
+PyPI = "https://pypi.org/project/kitconcept.seo"
+Source = "https://github.com/kitconcept/kitconcept.seo"
+Tracker = "https://github.com/kitconcept/kitconcept.seo/issues"
+
+
+[project.entry-points."plone.autoinclude.plugin"]
+target = "plone"
+
+[tool.uv]
+managed = false
+
+[tool.hatch.version]
+path = "src/kitconcept/seo/__init__.py"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build]
+strict-naming = true
+
+[tool.hatch.build.targets.sdist]
+exclude = [
+ "/.github",
+]
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/kitconcept"]
+
[tool.towncrier]
directory = "news/"
-filename = "CHANGES.md"
+filename = "CHANGELOG.md"
start_string = "\n"
title_format = "## {version} ({project_date})"
+template = "news/.changelog_template.jinja"
+issue_format = "[#{issue}](https://github.com/kitconcept/kitconcept.seo/issues/{issue})"
underlines = ["", "", ""]
[[tool.towncrier.type]]
@@ -38,124 +120,68 @@ directory = "tests"
name = "Tests"
showcontent = true
-##
-# Add extra configuration options in .meta.toml:
-# [pyproject]
-# towncrier_extra_lines = """
-# extra_configuration
-# """
-##
-
-[tool.isort]
-profile = "plone"
-
-##
-# Add extra configuration options in .meta.toml:
-# [pyproject]
-# isort_extra_lines = """
-# extra_configuration
-# """
-##
-
-[tool.black]
-target-version = ["py38"]
-
-##
-# Add extra configuration options in .meta.toml:
-# [pyproject]
-# black_extra_lines = """
-# extra_configuration
-# """
-##
-
-[tool.codespell]
-ignore-words-list = "discreet,vew"
-skip = "*.po,*.min.js"
-##
-# Add extra configuration options in .meta.toml:
-# [pyproject]
-# codespell_ignores = "foo,bar"
-# codespell_skip = "*.po,*.map,package-lock.json"
-##
-
-[tool.dependencychecker]
-Zope = [
- # Zope own provided namespaces
- 'App', 'OFS', 'Products.Five', 'Products.OFSP', 'Products.PageTemplates',
- 'Products.SiteAccess', 'Shared', 'Testing', 'ZPublisher', 'ZTUtils',
- 'Zope2', 'webdav', 'zmi',
- # ExtensionClass own provided namespaces
- 'ExtensionClass', 'ComputedAttribute', 'MethodObject',
- # Zope dependencies
- 'AccessControl', 'Acquisition', 'AuthEncoding', 'beautifulsoup4', 'BTrees',
- 'cffi', 'Chameleon', 'DateTime', 'DocumentTemplate',
- 'MultiMapping', 'multipart', 'PasteDeploy', 'Persistence', 'persistent',
- 'pycparser', 'python-gettext', 'pytz', 'RestrictedPython', 'roman',
- 'soupsieve', 'transaction', 'waitress', 'WebOb', 'WebTest', 'WSGIProxy2',
- 'z3c.pt', 'zc.lockfile', 'ZConfig', 'zExceptions', 'ZODB', 'zodbpickle',
- 'zope.annotation', 'zope.browser', 'zope.browsermenu', 'zope.browserpage',
- 'zope.browserresource', 'zope.cachedescriptors', 'zope.component',
- 'zope.configuration', 'zope.container', 'zope.contentprovider',
- 'zope.contenttype', 'zope.datetime', 'zope.deferredimport',
- 'zope.deprecation', 'zope.dottedname', 'zope.event', 'zope.exceptions',
- 'zope.filerepresentation', 'zope.globalrequest', 'zope.hookable',
- 'zope.i18n', 'zope.i18nmessageid', 'zope.interface', 'zope.lifecycleevent',
- 'zope.location', 'zope.pagetemplate', 'zope.processlifetime', 'zope.proxy',
- 'zope.ptresource', 'zope.publisher', 'zope.schema', 'zope.security',
- 'zope.sequencesort', 'zope.site', 'zope.size', 'zope.structuredtext',
- 'zope.tal', 'zope.tales', 'zope.testbrowser', 'zope.testing',
- 'zope.traversing', 'zope.viewlet'
+[tool.ruff]
+target-version = "py310"
+line-length = 88
+fix = true
+lint.select = [
+ # flake8-2020
+ "YTT",
+ # flake8-bandit
+ "S",
+ # flake8-bugbear
+ "B",
+ # flake8-builtins
+ "A",
+ # flake8-comprehensions
+ "C4",
+ # flake8-debugger
+ "T10",
+ # flake8-simplify
+ "SIM",
+ # mccabe
+ "C90",
+ # pycodestyle
+ "E", "W",
+ # pyflakes
+ "F",
+ # pygrep-hooks
+ "PGH",
+ # pyupgrade
+ "UP",
+ # ruff
+ "RUF",
]
-'Products.CMFCore' = [
- 'docutils', 'five.localsitemanager', 'Missing', 'Products.BTreeFolder2',
- 'Products.GenericSetup', 'Products.MailHost', 'Products.PythonScripts',
- 'Products.StandardCacheManagers', 'Products.ZCatalog', 'Record',
- 'zope.sendmail', 'Zope'
+lint.ignore = [
+ # DoNotAssignLambda
+ "E731",
]
-'plone.base' = [
- 'plone.batching', 'plone.registry', 'plone.schema','plone.z3cform',
- 'Products.CMFCore', 'Products.CMFDynamicViewFTI',
-]
-python-dateutil = ['dateutil']
-ignore-packages = ['zestreleaser.towncrier', 'zest.releaser', 'pytest', 'pytest-cov', 'pytest-plone', 'plone.app.iterate']
-'plone.app.testing' = ['plone.testing']
-'Products.CMFPlone' = ['Products.CMFCore', 'Products.GenericSetup']
-
-##
-# Add extra configuration options in .meta.toml:
-# [pyproject]
-# dependencies_ignores = "['zestreleaser.towncrier']"
-# dependencies_mappings = [
-# "gitpython = ['git']",
-# "pygithub = ['github']",
-# ]
-# """
-##
-
-[tool.check-manifest]
-ignore = [
- ".editorconfig",
- ".meta.toml",
- ".pre-commit-config.yaml",
- "tox.ini",
- ".flake8",
- "mx.ini",
+[tool.ruff.lint.isort]
+case-sensitive = false
+no-sections = true
+force-single-line = true
+from-first = true
+lines-after-imports = 2
+lines-between-types = 1
+order-by-type = false
+
+[tool.ruff.format]
+preview = true
+
+[tool.ruff.lint.per-file-ignores]
+"tests/*" = ["E501", "RUF001", "S101"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+
+[tool.coverage.run]
+source_pkgs = ["kitconcept.seo", "tests"]
+branch = true
+parallel = true
+omit = [
+ "src/kitconcept/seo/locales/*.py",
]
-##
-# Add extra configuration options in .meta.toml:
-# [pyproject]
-# check_manifest_ignores = """
-# "*.map.js",
-# "*.pyc",
-# """
-##
-
-
-##
-# Add extra configuration options in .meta.toml:
-# [pyproject]
-# extra_lines = """
-# _your own configuration lines_
-# """
-##
+
+[tool.zest-releaser]
+python-file-with-version = "src/kitconcept/seo/__init__.py"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 9dd6dd4..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
--c constraints.txt
--e ".[test]"
diff --git a/scripts/create_site.py b/scripts/create_site.py
new file mode 100644
index 0000000..553a968
--- /dev/null
+++ b/scripts/create_site.py
@@ -0,0 +1,66 @@
+from AccessControl.SecurityManagement import newSecurityManager
+from kitconcept.seo.interfaces import IKitconceptSeoLayer
+from Products.CMFPlone.factory import _DEFAULT_PROFILE
+from Products.CMFPlone.factory import addPloneSite
+from Products.GenericSetup.tool import SetupTool
+from Testing.makerequest import makerequest
+from zope.interface import directlyProvidedBy
+from zope.interface import directlyProvides
+
+import os
+import transaction
+
+
+truthy = frozenset(("t", "true", "y", "yes", "on", "1"))
+
+
+def asbool(s):
+ """Return the boolean value ``True`` if the case-lowered value of string
+ input ``s`` is a :term:`truthy string`. If ``s`` is already one of the
+ boolean values ``True`` or ``False``, return it."""
+ if s is None:
+ return False
+ if isinstance(s, bool):
+ return s
+ s = str(s).strip()
+ return s.lower() in truthy
+
+
+DELETE_EXISTING = asbool(os.getenv("DELETE_EXISTING"))
+
+app = makerequest(globals()["app"])
+
+request = app.REQUEST
+
+ifaces = [IKitconceptSeoLayer]
+for iface in directlyProvidedBy(request):
+ ifaces.append(iface)
+
+directlyProvides(request, *ifaces)
+
+admin = app.acl_users.getUserById("admin")
+admin = admin.__of__(app.acl_users)
+newSecurityManager(None, admin)
+
+site_id = "Plone"
+payload = {
+ "title": "kitconcept.seo",
+ "profile_id": _DEFAULT_PROFILE,
+ "distribution_name": "volto",
+ "setup_content": False,
+ "default_language": "en",
+ "portal_timezone": "UTC",
+}
+
+if site_id in app.objectIds() and DELETE_EXISTING:
+ app.manage_delObjects([site_id])
+ transaction.commit()
+ app._p_jar.sync()
+
+if site_id not in app.objectIds():
+ site = addPloneSite(app, site_id, **payload)
+ transaction.commit()
+
+ portal_setup: SetupTool = site.portal_setup
+ portal_setup.runAllImportStepsFromProfile("profile-kitconcept.seo:default")
+ transaction.commit()
diff --git a/setup.py b/setup.py
deleted file mode 100644
index cc6ee29..0000000
--- a/setup.py
+++ /dev/null
@@ -1,77 +0,0 @@
-"""Installer for the kitconcept.seo package."""
-
-from pathlib import Path
-from setuptools import setup
-
-
-long_description = f"""
-{Path("README.md").read_text()}\n
-{Path("CONTRIBUTORS.md").read_text()}\n
-{Path("CHANGES.md").read_text()}\n
-"""
-
-
-setup(
- name="kitconcept.seo",
- version="2.1.4.dev0",
- description="SEO optimizations plugin for Plone",
- long_description=long_description,
- long_description_content_type="text/markdown",
- classifiers=[
- "Development Status :: 5 - Production/Stable",
- "Environment :: Web Environment",
- "Framework :: Plone",
- "Framework :: Plone :: Addon",
- "Framework :: Plone :: 6.0",
- "Programming Language :: Python",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Operating System :: OS Independent",
- ],
- keywords="Python Plone CMS",
- author="kitconcept GmbH",
- author_email="info@kitconcept.com",
- url="https://pypi.python.org/pypi/kitconcept.seo",
- project_urls={
- "PyPI": "https://pypi.python.org/pypi/kitconcept.seo",
- "Source": "https://github.com/kitconcept/kitconcept.seo",
- "Tracker": "https://github.com/kitconcept/kitconcept.seo/issues",
- },
- license="GPL version 2",
- include_package_data=True,
- zip_safe=False,
- python_requires=">=3.8",
- install_requires=[
- "setuptools",
- "Products.CMFPlone",
- "plone.app.dexterity",
- "plone.autoform",
- "plone.behavior",
- "plone.dexterity",
- "plone.namedfile",
- "plone.supermodel",
- ],
- extras_require={
- "test": [
- "zest.releaser[recommended]",
- "zestreleaser.towncrier",
- "plone.app.contenttypes[test]",
- "plone.app.testing",
- "plone.restapi[test]",
- # Undeclared dependency of plone.restapi,
- # can be removed after next release
- "plone.app.iterate",
- "pytest",
- "pytest-cov",
- "pytest-plone>=0.2.0",
- ],
- },
- entry_points="""
- [z3c.autoinclude.plugin]
- target = plone
- [console_scripts]
- update_dist_locale = kitconcept.seo.locales.update:update_locale
- """,
-)
diff --git a/src/kitconcept/__init__.py b/src/kitconcept/__init__.py
deleted file mode 100644
index 26cfe40..0000000
--- a/src/kitconcept/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from pkgutil import extend_path
-
-
-__path__ = extend_path(__path__, __name__)
diff --git a/src/kitconcept/seo/__init__.py b/src/kitconcept/seo/__init__.py
index e3d5170..1366aef 100644
--- a/src/kitconcept/seo/__init__.py
+++ b/src/kitconcept/seo/__init__.py
@@ -1,9 +1,12 @@
"""Init and utils."""
+
from zope.i18nmessageid import MessageFactory
import logging
+__version__ = "3.0.0a0.dev0"
+
PACKAGE_NAME = "kitconcept.seo"
_ = MessageFactory(PACKAGE_NAME)
diff --git a/src/kitconcept/seo/behaviors/seo.py b/src/kitconcept/seo/behaviors/seo.py
index aaf612c..eac085a 100644
--- a/src/kitconcept/seo/behaviors/seo.py
+++ b/src/kitconcept/seo/behaviors/seo.py
@@ -20,9 +20,6 @@ class ISeo(model.Schema):
"seo_description",
"seo_noindex",
"seo_canonical_url",
- # "seo_nofollow",
- # "seo_noarchive",
- # "seo_nosnippet",
"opengraph_title",
"opengraph_description",
"opengraph_image",
@@ -74,40 +71,17 @@ class ISeo(model.Schema):
seo_canonical_url = schema.URI(
title=_("Canonical URL"),
description=_(
- "Tells the search engine to choose this URL as the canonical version and crawl that."
+ "Tells the search engine to choose this URL as the canonical "
+ "version and crawl that."
),
required=False,
)
- # # https://support.google.com/webmasters/answer/96569?hl=en
- # seo_nofollow = schema.Bool(
- # title=_(u"No Follow"),
- # description=_(u"Prevents search engines to follow links on this page"),
- # required=False,
- # )
-
- # # https://support.google.com/webmasters/answer/79812?hl=en
- # seo_noarchive = schema.Bool(
- # title=_(u"No Archive"),
- # description=_(
- # u"Prevents search engines to store a cached copy of this page"),
- # required=False,
- # )
-
- # # https://support.google.com/webmasters/answer/96569?hl=en
- # seo_nosnippet = schema.Bool(
- # title=_(u"No Snippet"),
- # description=_(
- # u"Prevents search engines from displaying a snippet for your page in search results" # noqa
- # ),
- # required=False,
- # )
-
opengraph_title = schema.TextLine(
title=_("Open Graph Title"),
description=_(
- "Override the Open Graph title, that Facebook and Twitter use. When empty the default title will "
- + "be used. Use maximum 60 characters."
+ "Override the Open Graph title, that Facebook and Twitter use. When empty "
+ "the default title will be used. Use maximum 60 characters."
),
required=False,
)
@@ -115,8 +89,9 @@ class ISeo(model.Schema):
opengraph_description = schema.TextLine(
title=_("Open Graph Description"),
description=_(
- "Override the Open Graph description, that Facebook and Twitter use. When empty the default "
- + "description will be used. Use maximum 155 characters."
+ "Override the Open Graph description, that Facebook and Twitter use. "
+ "When empty the default description will be used. "
+ "Use maximum 155 characters."
),
required=False,
)
@@ -124,8 +99,9 @@ class ISeo(model.Schema):
opengraph_image = NamedBlobImage(
title=_("Open Graph Image"),
description=_(
- "Override the Open Graph image, that Facebook and Twitter use. When empty the default "
- + "lead image will be used. Recommended image ratio is 1,91:1 and 1200 x 630px."
+ "Override the Open Graph image, that Facebook and Twitter use. "
+ "When empty the default lead image will be used. Recommended image "
+ "ratio is 1.91:1 and 1200 x 630px."
),
required=False,
)
diff --git a/src/kitconcept/seo/locales/__main__.py b/src/kitconcept/seo/locales/__main__.py
new file mode 100644
index 0000000..a5677b6
--- /dev/null
+++ b/src/kitconcept/seo/locales/__main__.py
@@ -0,0 +1,69 @@
+"""Update locales."""
+
+from pathlib import Path
+
+import logging
+import re
+import subprocess
+
+
+logger = logging.getLogger("i18n")
+logger.setLevel(logging.DEBUG)
+
+
+PATTERN = r"^[a-z]{2}.*"
+
+locale_path = Path(__file__).parent.resolve()
+target_path = locale_path.parent.resolve()
+domains = [path.name[:-4] for path in locale_path.glob("*.pot")]
+
+i18ndude = "uvx i18ndude"
+
+# ignore node_modules files resulting in errors
+excludes = '"*.html *json-schema*.xml"'
+
+
+def locale_folder_setup(domain: str):
+ languages = [path for path in locale_path.glob("*") if path.is_dir()]
+ for lang_folder in languages:
+ lc_messages_path = lang_folder / "LC_MESSAGES"
+ lang = lang_folder.name
+ if lc_messages_path.exists():
+ continue
+ elif re.match(PATTERN, lang):
+ lc_messages_path.mkdir()
+ cmd = (
+ f"msginit --locale={lang} "
+ f"--input={locale_path}/{domain}.pot "
+ f"--output={locale_path}/{lang}/LC_MESSAGES/{domain}.po"
+ )
+ subprocess.call(cmd, shell=True) # noQA: S602
+
+
+def _rebuild(domain: str):
+ cmd = (
+ f"{i18ndude} rebuild-pot --pot {locale_path}/{domain}.pot "
+ f"--exclude {excludes} "
+ f"--create {domain} {target_path}"
+ )
+ subprocess.call(cmd, shell=True) # noQA: S602
+
+
+def _sync(domain: str):
+ cmd = (
+ f"{i18ndude} sync --pot {locale_path}/{domain}.pot "
+ f"{locale_path}/*/LC_MESSAGES/{domain}.po"
+ )
+ subprocess.call(cmd, shell=True) # noQA: S602
+
+
+def main():
+ for domain in domains:
+ logger.info(f"Updating translations for {domain}")
+ locale_folder_setup(domain)
+ _rebuild(domain)
+ _sync(domain)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/kitconcept/seo/locales/de/LC_MESSAGES/kitconcept.seo.po b/src/kitconcept/seo/locales/de/LC_MESSAGES/kitconcept.seo.po
index b4d365a..3e1193a 100644
--- a/src/kitconcept/seo/locales/de/LC_MESSAGES/kitconcept.seo.po
+++ b/src/kitconcept/seo/locales/de/LC_MESSAGES/kitconcept.seo.po
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
-"POT-Creation-Date: 2020-03-24 14:11+0000\n"
+"POT-Creation-Date: 2026-01-28 12:10+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -14,87 +14,86 @@ msgstr ""
"Preferred-Encodings: utf-8 latin1\n"
"Domain: DOMAIN\n"
-#: kitconcept/seo/behaviors/seo.py:62
+#: kitconcept/seo/behaviors/seo.py:72
msgid "Canonical URL"
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:44
+#: kitconcept/seo/behaviors/seo.py:55
msgid "Description"
msgstr "Beschreibung"
-#: kitconcept/seo/behaviors/configure.zcml:17
+#: kitconcept/seo/behaviors/configure.zcml:21
msgid "Enhances a content type with fields for Search Engine Optimizations"
msgstr "Erweitert einen Inhaltstyp um Felder für die Suchmaschinenoptimierung"
-#: kitconcept/seo/configure.zcml:22
+#: kitconcept/seo/profiles.zcml:13
msgid "Installs the kitconcept.seo add-on."
msgstr "Installiert das kitconcept.seo Add-on"
-#: kitconcept/seo/behaviors/seo.py:55
+#: kitconcept/seo/behaviors/seo.py:65
msgid "No Index"
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:104
+#: kitconcept/seo/behaviors/seo.py:90
msgid "Open Graph Description"
msgstr "Open Graph Beschreibung"
-#: kitconcept/seo/behaviors/seo.py:114
+#: kitconcept/seo/behaviors/seo.py:100
msgid "Open Graph Image"
msgstr "Open Graph Bild"
-#: kitconcept/seo/behaviors/seo.py:94
+#: kitconcept/seo/behaviors/seo.py:81
msgid "Open Graph Title"
msgstr "Open Graph Titel"
-#: kitconcept/seo/behaviors/seo.py:105
+#: kitconcept/seo/behaviors/seo.py:91
msgid "Override the Open Graph description, that Facebook and Twitter use. When empty the default description will be used. Use maximum 155 characters."
msgstr "Überschreibt die Open Graph Beschreibung, die von Facebook und Twitter genutzt wird. Wenn dieses Feld leer ist wird das Standard Beschreibung Feld verwendet. Empfohlene Länge: 155 Zeichen."
-#: kitconcept/seo/behaviors/seo.py:115
-msgid "Override the Open Graph image, that Facebook and Twitter use. When empty the default lead image will be used. Recommended image ratio is 1,91:1 and 1200 x 630px."
-msgstr "Überschreibt das Open Graph Bild, welches von Facebook und Twitter genutzt wird. Wenn dieses Feld leer ist wird das Lead Image Feld verwendet."
-
+#: kitconcept/seo/behaviors/seo.py:101
+msgid "Override the Open Graph image, that Facebook and Twitter use. When empty the default lead image will be used. Recommended image ratio is 1.91:1 and 1200 x 630px."
+msgstr ""
-#: kitconcept/seo/behaviors/seo.py:95
+#: kitconcept/seo/behaviors/seo.py:82
msgid "Override the Open Graph title, that Facebook and Twitter use. When empty the default title will be used. Use maximum 60 characters."
msgstr "Überschreibt den Open Graph Titel, der von Facebook und Twitter genutzt wird. Wenn dieses Feld leer ist wird das Standard Titel Feld verwendet. Empfohlene Länge: 60 Zeichen."
-#: kitconcept/seo/behaviors/seo.py:45
-msgid "Override the meta description. When empty the default description will be used. Use maximum 150 characters."
-msgstr "Überschreibt die Meta-Beschreibung."
+#: kitconcept/seo/behaviors/seo.py:56
+msgid "Override the meta description. When empty the default description will be used. Use maximum 155 characters."
+msgstr ""
-#: kitconcept/seo/behaviors/seo.py:35
-msgid "Override the meta title. When empty the default title will be used. Use maximum 50 characters."
-msgstr "Überschreibt den Meta-Titel."
+#: kitconcept/seo/behaviors/seo.py:39
+msgid "Override the meta title. When empty the default title will be used. Use maximum 55 characters."
+msgstr ""
-#: kitconcept/seo/behaviors/seo.py:56
+#: kitconcept/seo/behaviors/seo.py:66
msgid "Prevents a page from appearing in search engines"
msgstr "Schließt die aktuelle Seite vom Suchmaschinen-Index aus."
-#: kitconcept/seo/behaviors/seo.py:18
+#: kitconcept/seo/behaviors/seo.py:17
msgid "SEO"
msgstr ""
-#: kitconcept/seo/behaviors/configure.zcml:17
+#: kitconcept/seo/behaviors/configure.zcml:21
msgid "SEO Behavior"
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:63
+#: kitconcept/seo/behaviors/seo.py:73
msgid "Tells the search engine to choose this URL as the canonical version and crawl that."
msgstr "Teil der Suchmaschine mit dass die hier hinterlegte URL die kanonische Version ist, die gecrawlt werden soll."
-#: kitconcept/seo/behaviors/seo.py:34
+#: kitconcept/seo/behaviors/seo.py:38
msgid "Title"
msgstr "Titel"
-#: kitconcept/seo/configure.zcml:31
+#: kitconcept/seo/profiles.zcml:21
msgid "Uninstalls the kitconcept.seo add-on."
msgstr "Deinstalliert das kitconcept.seo Add-On"
-#: kitconcept/seo/configure.zcml:22
-msgid "kitconcept.seo"
+#: kitconcept/seo/profiles.zcml:13
+msgid "kitconcept seo: Install"
msgstr ""
-#: kitconcept/seo/configure.zcml:31
-msgid "kitconcept.seo (uninstall)"
+#: kitconcept/seo/profiles.zcml:21
+msgid "kitconcept seo: Uninstall"
msgstr ""
diff --git a/src/kitconcept/seo/locales/es/LC_MESSAGES/kitconcept.seo.po b/src/kitconcept/seo/locales/es/LC_MESSAGES/kitconcept.seo.po
index 34efcb5..ed80edd 100644
--- a/src/kitconcept/seo/locales/es/LC_MESSAGES/kitconcept.seo.po
+++ b/src/kitconcept/seo/locales/es/LC_MESSAGES/kitconcept.seo.po
@@ -3,11 +3,10 @@
msgid ""
msgstr ""
"Project-Id-Version: kitconcept.seo\n"
-"POT-Creation-Date: 2020-03-24 14:11+0000\n"
+"POT-Creation-Date: 2026-01-28 12:10+0000\n"
"PO-Revision-Date: 2025-07-16 12:42+0200\n"
"Last-Translator: Leonardo J. Caballero G. \n"
"Language-Team: Plone i18n \n"
-"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
@@ -16,89 +15,90 @@ msgstr ""
"Language-Name: Español\n"
"Preferred-Encodings: utf-8 latin1\n"
"Domain: kitconcept.seo\n"
+"Language: es\n"
"X-Generator: Poedit 3.6\n"
"X-Is-Fallback-For: es-ar es-bo es-cl es-co es-cr es-do es-ec es-es es-sv es-gt es-hn es-mx es-ni es-pa es-py es-pe es-pr es-us es-uy es-ve\n"
-#: kitconcept/seo/behaviors/seo.py:62
+#: kitconcept/seo/behaviors/seo.py:72
msgid "Canonical URL"
msgstr "URL canónica"
-#: kitconcept/seo/behaviors/seo.py:44
+#: kitconcept/seo/behaviors/seo.py:55
msgid "Description"
msgstr "Descripción"
-#: kitconcept/seo/behaviors/configure.zcml:17
+#: kitconcept/seo/behaviors/configure.zcml:21
msgid "Enhances a content type with fields for Search Engine Optimizations"
msgstr "Añade campos adicionales a los tipos de contenido para SEO"
-#: kitconcept/seo/configure.zcml:22
+#: kitconcept/seo/profiles.zcml:13
msgid "Installs the kitconcept.seo add-on."
msgstr "Instala el complemento kitconcept.seo."
-#: kitconcept/seo/behaviors/seo.py:55
+#: kitconcept/seo/behaviors/seo.py:65
msgid "No Index"
msgstr "Sin índice"
-#: kitconcept/seo/behaviors/seo.py:104
+#: kitconcept/seo/behaviors/seo.py:90
msgid "Open Graph Description"
msgstr "Descripción de Open Graph"
-#: kitconcept/seo/behaviors/seo.py:114
+#: kitconcept/seo/behaviors/seo.py:100
msgid "Open Graph Image"
msgstr "Imagen Open Graph"
-#: kitconcept/seo/behaviors/seo.py:94
+#: kitconcept/seo/behaviors/seo.py:81
msgid "Open Graph Title"
msgstr "Título de Open Graph"
-#: kitconcept/seo/behaviors/seo.py:105
+#: kitconcept/seo/behaviors/seo.py:91
msgid "Override the Open Graph description, that Facebook and Twitter use. When empty the default description will be used. Use maximum 155 characters."
msgstr "Sobrescribe la descripción para Open Graph, utilizada por Facebook y Twitter. Si está vacía, se utilizará la descripción del contenido. Debe utilizarse un máximo de 155 caracteres."
-#: kitconcept/seo/behaviors/seo.py:115
-msgid "Override the Open Graph image, that Facebook and Twitter use. When empty the default lead image will be used. Recommended image ratio is 1,91:1 and 1200 x 630px."
-msgstr "Sobrescribe la imagen para Open Graph, utilizada por Facebook y Twitter. Si está vacía, se utilizará la imagen de contenido (si está presente). La proporción recomendada es 1,91:1 y 1200 x 630px."
+#: kitconcept/seo/behaviors/seo.py:101
+msgid "Override the Open Graph image, that Facebook and Twitter use. When empty the default lead image will be used. Recommended image ratio is 1.91:1 and 1200 x 630px."
+msgstr ""
-#: kitconcept/seo/behaviors/seo.py:95
+#: kitconcept/seo/behaviors/seo.py:82
msgid "Override the Open Graph title, that Facebook and Twitter use. When empty the default title will be used. Use maximum 60 characters."
msgstr "Sobrescribe el título para Open Graph, utilizado por Facebook y Twitter. Si está vacío, se utilizará el título del contenido. Debe utilizarse un máximo de 60 caracteres."
-#: kitconcept/seo/behaviors/seo.py:45
-msgid "Override the meta description. When empty the default description will be used. Use maximum 150 characters."
-msgstr "Sobrescribe la etiqueta meta description. Si está vacía, se utilizará la descripción del contenido. Debe utilizarse un máximo de 150 caracteres."
+#: kitconcept/seo/behaviors/seo.py:56
+msgid "Override the meta description. When empty the default description will be used. Use maximum 155 characters."
+msgstr ""
-#: kitconcept/seo/behaviors/seo.py:35
-msgid "Override the meta title. When empty the default title will be used. Use maximum 50 characters."
-msgstr "Sobrescribe la etiqueta meta title. Si está vacía, se utilizará el título del contenido. Debe utilizarse un máximo de 50 caracteres."
+#: kitconcept/seo/behaviors/seo.py:39
+msgid "Override the meta title. When empty the default title will be used. Use maximum 55 characters."
+msgstr ""
-#: kitconcept/seo/behaviors/seo.py:56
+#: kitconcept/seo/behaviors/seo.py:66
msgid "Prevents a page from appearing in search engines"
msgstr "Impide que la página sea indexada por los motores de búsqueda"
-#: kitconcept/seo/behaviors/seo.py:18
+#: kitconcept/seo/behaviors/seo.py:17
msgid "SEO"
msgstr "SEO"
-#: kitconcept/seo/behaviors/configure.zcml:17
+#: kitconcept/seo/behaviors/configure.zcml:21
msgid "SEO Behavior"
msgstr "Comportamiento SEO"
-#: kitconcept/seo/behaviors/seo.py:63
+#: kitconcept/seo/behaviors/seo.py:73
msgid "Tells the search engine to choose this URL as the canonical version and crawl that."
msgstr "Indica a los motores de búsqueda que utilicen la URL proporcionada como versión canónica del contenido actual, y que indexen ese contenido en consecuencia."
-#: kitconcept/seo/behaviors/seo.py:34
+#: kitconcept/seo/behaviors/seo.py:38
msgid "Title"
msgstr "Título"
-#: kitconcept/seo/configure.zcml:31
+#: kitconcept/seo/profiles.zcml:21
msgid "Uninstalls the kitconcept.seo add-on."
msgstr "Desinstala el complemento kitconcept.seo."
-#: kitconcept/seo/configure.zcml:22
-msgid "kitconcept.seo"
-msgstr "kitconcept.seo"
+#: kitconcept/seo/profiles.zcml:13
+msgid "kitconcept seo: Install"
+msgstr ""
-#: kitconcept/seo/configure.zcml:31
-msgid "kitconcept.seo (uninstall)"
-msgstr "kitconcept.seo (desinstalar)"
+#: kitconcept/seo/profiles.zcml:21
+msgid "kitconcept seo: Uninstall"
+msgstr ""
diff --git a/src/kitconcept/seo/locales/it/LC_MESSAGES/kitconcept.seo.po b/src/kitconcept/seo/locales/it/LC_MESSAGES/kitconcept.seo.po
index 02de6d2..1bc917e 100644
--- a/src/kitconcept/seo/locales/it/LC_MESSAGES/kitconcept.seo.po
+++ b/src/kitconcept/seo/locales/it/LC_MESSAGES/kitconcept.seo.po
@@ -4,7 +4,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
-"POT-Creation-Date: 2020-03-24 14:11+0000\n"
+"POT-Creation-Date: 2026-01-28 12:10+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -17,86 +17,86 @@ msgstr ""
"Preferred-Encodings: utf-8 latin1\n"
"Domain: kitconcept.seo\n"
-#: kitconcept/seo/behaviors/seo.py:62
+#: kitconcept/seo/behaviors/seo.py:72
msgid "Canonical URL"
msgstr "Canonical URL"
-#: kitconcept/seo/behaviors/seo.py:44
+#: kitconcept/seo/behaviors/seo.py:55
msgid "Description"
msgstr "Descrizione"
-#: kitconcept/seo/behaviors/configure.zcml:17
+#: kitconcept/seo/behaviors/configure.zcml:21
msgid "Enhances a content type with fields for Search Engine Optimizations"
msgstr "Aggiunge dei campi extra ai content type per il SEO"
-#: kitconcept/seo/configure.zcml:22
+#: kitconcept/seo/profiles.zcml:13
msgid "Installs the kitconcept.seo add-on."
msgstr "Installa kitconcept.seo"
-#: kitconcept/seo/behaviors/seo.py:55
+#: kitconcept/seo/behaviors/seo.py:65
msgid "No Index"
msgstr "No Index"
-#: kitconcept/seo/behaviors/seo.py:104
+#: kitconcept/seo/behaviors/seo.py:90
msgid "Open Graph Description"
msgstr "Descrizione Open Graph"
-#: kitconcept/seo/behaviors/seo.py:114
+#: kitconcept/seo/behaviors/seo.py:100
msgid "Open Graph Image"
msgstr "Immagine Open Graph"
-#: kitconcept/seo/behaviors/seo.py:94
+#: kitconcept/seo/behaviors/seo.py:81
msgid "Open Graph Title"
msgstr "Titolo Open Graph"
-#: kitconcept/seo/behaviors/seo.py:105
+#: kitconcept/seo/behaviors/seo.py:91
msgid "Override the Open Graph description, that Facebook and Twitter use. When empty the default description will be used. Use maximum 155 characters."
msgstr "Sovrascrive la descrizione per Open Graph, utilizzata da Facebook e Twitter. Se vuoto, verrà utilizzata la descrizione del contenuto. Sarebbe indicato utilizzare al massimo 155 caratteri."
-#: kitconcept/seo/behaviors/seo.py:115
-msgid "Override the Open Graph image, that Facebook and Twitter use. When empty the default lead image will be used. Recommended image ratio is 1,91:1 and 1200 x 630px."
-msgstr "Sovrascrive l'immagine per Open Graph, utilizzata da Facebook e Twitter. Se vuoto, verrà utilizzata l'immagine del contenuto (se presente). La proporzione raccomandata è 1,91:1 e 1200 x 630px."
+#: kitconcept/seo/behaviors/seo.py:101
+msgid "Override the Open Graph image, that Facebook and Twitter use. When empty the default lead image will be used. Recommended image ratio is 1.91:1 and 1200 x 630px."
+msgstr ""
-#: kitconcept/seo/behaviors/seo.py:95
+#: kitconcept/seo/behaviors/seo.py:82
msgid "Override the Open Graph title, that Facebook and Twitter use. When empty the default title will be used. Use maximum 60 characters."
msgstr "Sovrascrive il titolo per Open Graph, utilizzato da Facebook e Twitter. Se vuoto, verrà utilizzato il titolo del contenuto. Sarebbe indicato utilizzare al massimo 60 caratteri."
-#: kitconcept/seo/behaviors/seo.py:45
-msgid "Override the meta description. When empty the default description will be used. Use maximum 150 characters."
-msgstr "Sovrascrive il meta-tag description. Se vuoto, verrà utilizzata la descrizione del contenuto. Sarebbe indicato utilizzare al massimo 150 caratteri."
+#: kitconcept/seo/behaviors/seo.py:56
+msgid "Override the meta description. When empty the default description will be used. Use maximum 155 characters."
+msgstr ""
-#: kitconcept/seo/behaviors/seo.py:35
-msgid "Override the meta title. When empty the default title will be used. Use maximum 50 characters."
-msgstr "Sovrascrive il meta-tag title. Se vuoto, verrà utilizzato il titolo del contenuto. Sarebbe indicato utilizzare al massimo 50 caratteri."
+#: kitconcept/seo/behaviors/seo.py:39
+msgid "Override the meta title. When empty the default title will be used. Use maximum 55 characters."
+msgstr ""
-#: kitconcept/seo/behaviors/seo.py:56
+#: kitconcept/seo/behaviors/seo.py:66
msgid "Prevents a page from appearing in search engines"
msgstr "Impedisce alla pagina di essere indicizzata dai motori di ricerca."
-#: kitconcept/seo/behaviors/seo.py:18
+#: kitconcept/seo/behaviors/seo.py:17
msgid "SEO"
msgstr "SEO"
-#: kitconcept/seo/behaviors/configure.zcml:17
+#: kitconcept/seo/behaviors/configure.zcml:21
msgid "SEO Behavior"
msgstr "SEO Behavior"
-#: kitconcept/seo/behaviors/seo.py:63
+#: kitconcept/seo/behaviors/seo.py:73
msgid "Tells the search engine to choose this URL as the canonical version and crawl that."
msgstr "Dice ai motori di ricerca di utilizzare l'URL fornito come versione canonica del contenuto corrente, e di conseguenza indicizzano quel contenuto."
-#: kitconcept/seo/behaviors/seo.py:34
+#: kitconcept/seo/behaviors/seo.py:38
msgid "Title"
msgstr "Titolo"
-#: kitconcept/seo/configure.zcml:31
+#: kitconcept/seo/profiles.zcml:21
msgid "Uninstalls the kitconcept.seo add-on."
msgstr "Disinstalla kitconcept.seo"
-#: kitconcept/seo/configure.zcml:22
-msgid "kitconcept.seo"
-msgstr "kitconcept.seo"
+#: kitconcept/seo/profiles.zcml:13
+msgid "kitconcept seo: Install"
+msgstr ""
-#: kitconcept/seo/configure.zcml:31
-msgid "kitconcept.seo (uninstall)"
-msgstr "kitconcept.seo (disinstalla)"
+#: kitconcept/seo/profiles.zcml:21
+msgid "kitconcept seo: Uninstall"
+msgstr ""
diff --git a/src/kitconcept/seo/locales/kitconcept.seo.pot b/src/kitconcept/seo/locales/kitconcept.seo.pot
index f992167..8188c1f 100644
--- a/src/kitconcept/seo/locales/kitconcept.seo.pot
+++ b/src/kitconcept/seo/locales/kitconcept.seo.pot
@@ -1,10 +1,10 @@
-# --- PLEASE EDIT THE LINES BELOW CORRECTLY ---
-# SOME DESCRIPTIVE TITLE.
-# FIRST AUTHOR , YEAR.
+#--- PLEASE EDIT THE LINES BELOW CORRECTLY ---
+#SOME DESCRIPTIVE TITLE.
+#FIRST AUTHOR , YEAR.
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
-"POT-Creation-Date: 2020-03-24 14:11+0000\n"
+"POT-Creation-Date: 2026-01-28 12:10+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -17,86 +17,86 @@ msgstr ""
"Preferred-Encodings: utf-8 latin1\n"
"Domain: kitconcept.seo\n"
-#: kitconcept/seo/behaviors/seo.py:62
+#: kitconcept/seo/behaviors/seo.py:72
msgid "Canonical URL"
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:44
+#: kitconcept/seo/behaviors/seo.py:55
msgid "Description"
msgstr ""
-#: kitconcept/seo/behaviors/configure.zcml:17
+#: kitconcept/seo/behaviors/configure.zcml:21
msgid "Enhances a content type with fields for Search Engine Optimizations"
msgstr ""
-#: kitconcept/seo/configure.zcml:22
+#: kitconcept/seo/profiles.zcml:13
msgid "Installs the kitconcept.seo add-on."
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:55
+#: kitconcept/seo/behaviors/seo.py:65
msgid "No Index"
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:104
+#: kitconcept/seo/behaviors/seo.py:90
msgid "Open Graph Description"
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:114
+#: kitconcept/seo/behaviors/seo.py:100
msgid "Open Graph Image"
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:94
+#: kitconcept/seo/behaviors/seo.py:81
msgid "Open Graph Title"
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:105
+#: kitconcept/seo/behaviors/seo.py:91
msgid "Override the Open Graph description, that Facebook and Twitter use. When empty the default description will be used. Use maximum 155 characters."
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:115
-msgid "Override the Open Graph image, that Facebook and Twitter use. When empty the default lead image will be used. Recommended image ratio is 1,91:1 and 1200 x 630px."
+#: kitconcept/seo/behaviors/seo.py:101
+msgid "Override the Open Graph image, that Facebook and Twitter use. When empty the default lead image will be used. Recommended image ratio is 1.91:1 and 1200 x 630px."
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:95
+#: kitconcept/seo/behaviors/seo.py:82
msgid "Override the Open Graph title, that Facebook and Twitter use. When empty the default title will be used. Use maximum 60 characters."
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:45
-msgid "Override the meta description. When empty the default description will be used. Use maximum 150 characters."
+#: kitconcept/seo/behaviors/seo.py:56
+msgid "Override the meta description. When empty the default description will be used. Use maximum 155 characters."
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:35
-msgid "Override the meta title. When empty the default title will be used. Use maximum 50 characters."
+#: kitconcept/seo/behaviors/seo.py:39
+msgid "Override the meta title. When empty the default title will be used. Use maximum 55 characters."
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:56
+#: kitconcept/seo/behaviors/seo.py:66
msgid "Prevents a page from appearing in search engines"
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:18
+#: kitconcept/seo/behaviors/seo.py:17
msgid "SEO"
msgstr ""
-#: kitconcept/seo/behaviors/configure.zcml:17
+#: kitconcept/seo/behaviors/configure.zcml:21
msgid "SEO Behavior"
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:63
+#: kitconcept/seo/behaviors/seo.py:73
msgid "Tells the search engine to choose this URL as the canonical version and crawl that."
msgstr ""
-#: kitconcept/seo/behaviors/seo.py:34
+#: kitconcept/seo/behaviors/seo.py:38
msgid "Title"
msgstr ""
-#: kitconcept/seo/configure.zcml:31
+#: kitconcept/seo/profiles.zcml:21
msgid "Uninstalls the kitconcept.seo add-on."
msgstr ""
-#: kitconcept/seo/configure.zcml:22
-msgid "kitconcept.seo"
+#: kitconcept/seo/profiles.zcml:13
+msgid "kitconcept seo: Install"
msgstr ""
-#: kitconcept/seo/configure.zcml:31
-msgid "kitconcept.seo (uninstall)"
+#: kitconcept/seo/profiles.zcml:21
+msgid "kitconcept seo: Uninstall"
msgstr ""
diff --git a/src/kitconcept/seo/locales/update.sh b/src/kitconcept/seo/locales/update.sh
deleted file mode 100755
index e171378..0000000
--- a/src/kitconcept/seo/locales/update.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/bash
-# i18ndude should be available in current $PATH (eg by running
-# ``export PATH=$PATH:$BUILDOUT_DIR/bin`` when i18ndude is located in your buildout's bin directory)
-#
-# For every language you want to translate into you need a
-# locales/[language]/LC_MESSAGES/kitconcept.seo.po
-# (e.g. locales/de/LC_MESSAGES/kitconcept.seo.po)
-
-domain=kitconcept.seo
-
-i18ndude rebuild-pot --pot $domain.pot --create $domain ../
-i18ndude sync --pot $domain.pot */LC_MESSAGES/$domain.po
diff --git a/src/kitconcept/seo/testing.py b/src/kitconcept/seo/testing.py
index 1754799..833a988 100644
--- a/src/kitconcept/seo/testing.py
+++ b/src/kitconcept/seo/testing.py
@@ -5,7 +5,7 @@
from plone.app.testing import PloneSandboxLayer
from plone.testing.zope import WSGI_SERVER_FIXTURE
-import kitconcept.seo # noQA
+import kitconcept.seo
class Layer(PloneSandboxLayer):
diff --git a/tests/behaviors/conftest.py b/tests/behaviors/conftest.py
new file mode 100644
index 0000000..f2d912b
--- /dev/null
+++ b/tests/behaviors/conftest.py
@@ -0,0 +1,83 @@
+from plone.app.testing import setRoles
+from plone.app.testing import SITE_OWNER_NAME
+from plone.app.testing import SITE_OWNER_PASSWORD
+from plone.app.testing import TEST_USER_ID
+from plone.dexterity.fti import DexterityFTI
+from plone.restapi.testing import RelativeSession
+from zope.component.hooks import setSite
+
+import pytest
+import transaction
+
+
+@pytest.fixture()
+def request_factory(portal):
+ def factory(mimetype: str = "application/json") -> RelativeSession:
+ url = portal.absolute_url()
+ api_session = RelativeSession(url)
+ api_session.headers.update({"Accept": mimetype})
+ return api_session
+
+ return factory
+
+
+@pytest.fixture()
+def anon_request(request_factory) -> RelativeSession:
+ return request_factory()
+
+
+@pytest.fixture()
+def manager_request(request_factory):
+ request = request_factory()
+ request.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)
+ yield request
+ request.auth = ()
+
+
+@pytest.fixture()
+def manager_html_request(request_factory):
+ request = request_factory("text/html")
+ request.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)
+ yield request
+ request.auth = ()
+
+
+@pytest.fixture
+def portal(functional):
+ return functional["portal"]
+
+
+@pytest.fixture
+def portal_factory(functional):
+ def func(behavior: str):
+ portal = functional["portal"]
+ setRoles(portal, TEST_USER_ID, ["Manager"])
+ fti = DexterityFTI("DummyType")
+ fti.behaviors = (behavior,)
+ portal.portal_types._setObject("DummyType", fti)
+ setSite(portal)
+ transaction.commit()
+ return portal
+
+ return func
+
+
+@pytest.fixture
+def dummy_type_schema(manager_request):
+ def func():
+ url = "/@types/DummyType"
+ response = manager_request.get(url)
+ data = response.json()
+ return data
+
+ return func
+
+
+@pytest.fixture
+def create_dummy_content(manager_request):
+ def func(payload: dict):
+ payload["@type"] = "DummyType"
+ response = manager_request.post("/", json=payload)
+ return response
+
+ return func
diff --git a/tests/behaviors/test_behavior.py b/tests/behaviors/test_behavior.py
new file mode 100644
index 0000000..ea76b88
--- /dev/null
+++ b/tests/behaviors/test_behavior.py
@@ -0,0 +1,57 @@
+from copy import deepcopy
+from kitconcept.seo import PACKAGE_NAME
+
+import pytest
+
+
+PAYLOAD = {
+ "seo_title": "Foo Bar",
+ "seo_description": "Lorem ipsum dolor sit amet",
+ "seo_noindex": False,
+ "seo_canonical_url": "https://example.com/canonical-url",
+ "opengraph_title": "Open Graph Title",
+ "opengraph_description": "Open Graph Description",
+ "opengraph_image": None,
+}
+
+
+@pytest.fixture
+def payload() -> dict:
+ data = deepcopy(PAYLOAD)
+ return data
+
+
+class TestBehaviorContato:
+ name: str = f"{PACKAGE_NAME}"
+
+ @pytest.fixture(autouse=True)
+ def _setup(self, portal_factory, dummy_type_schema, manager_html_request):
+ self.portal = portal_factory(behavior=self.name)
+ self.schema = dummy_type_schema()
+ self.request = manager_html_request
+
+ @pytest.mark.parametrize("key", PAYLOAD.keys())
+ def test_behavior_schema(self, key: str):
+ assert key in self.schema["properties"]
+
+ def test_behavior_data(self, payload: dict, create_dummy_content):
+ response = create_dummy_content(payload)
+ assert response.status_code == 201
+
+ def test_noindex_sets_response_header(self):
+ # Add page
+ resp = self.request.post(
+ "++api++/",
+ json={"@type": "DummyType", "title": "Test page", "seo_noindex": True},
+ )
+ assert resp.status_code == 201
+
+ # Confirm the page is served with X-Robots-Tag header
+ resp = self.request.get("/test-page")
+ assert resp.status_code == 200
+ assert resp.headers["X-Robots-Tag"] == "noindex"
+
+ # Confirm views of the page are served with X-Robots-Tag header
+ resp = self.request.get("/test-page/@@view")
+ assert resp.status_code == 200
+ assert resp.headers["X-Robots-Tag"] == "noindex"
diff --git a/tests/conftest.py b/tests/conftest.py
index 1a31697..a7d93b1 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,29 +1,14 @@
from kitconcept.seo.testing import FUNCTIONAL_TESTING
from kitconcept.seo.testing import INTEGRATION_TESTING
-from plone.app.testing import SITE_OWNER_NAME
-from plone.app.testing import SITE_OWNER_PASSWORD
-from plone.restapi.testing import RelativeSession
from pytest_plone import fixtures_factory
-import pytest
-
pytest_plugins = ["pytest_plone"]
globals().update(
- fixtures_factory(
- (
- (FUNCTIONAL_TESTING, "functional"),
- (INTEGRATION_TESTING, "integration"),
- )
- )
+ fixtures_factory((
+ (FUNCTIONAL_TESTING, "functional"),
+ (INTEGRATION_TESTING, "integration"),
+ ))
)
-
-
-@pytest.fixture
-def manager_plone_client(functional):
- portal = functional["portal"]
- api_session = RelativeSession(f"{portal.absolute_url()}")
- api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)
- return api_session
diff --git a/tests/test_behavior.py b/tests/test_behavior.py
deleted file mode 100644
index abd93a8..0000000
--- a/tests/test_behavior.py
+++ /dev/null
@@ -1,47 +0,0 @@
-def test_seo_behavior_fields(manager_plone_client):
- # Enable behavior for pages
- manager_plone_client.patch(
- "++api++/@controlpanels/dexterity-types/Document", json={"kitconcept.seo": True}
- )
-
- # Check schema
- schema = manager_plone_client.get("++api++/@types/Document").json()
- assert schema["fieldsets"][-1] == {
- "behavior": "plone",
- "description": "",
- "fields": [
- "seo_title",
- "seo_description",
- "seo_noindex",
- "seo_canonical_url",
- "opengraph_title",
- "opengraph_description",
- "opengraph_image",
- ],
- "id": "seo",
- "title": "SEO",
- }
-
-
-def test_noindex_sets_response_header(manager_plone_client):
- # Enable behavior for pages
- manager_plone_client.patch(
- "++api++/@controlpanels/dexterity-types/Document", json={"kitconcept.seo": True}
- )
-
- # Add page
- resp = manager_plone_client.post(
- "++api++/",
- json={"@type": "Document", "title": "Test page", "seo_noindex": True},
- )
- assert resp.status_code == 201
-
- # Confirm the page is served with X-Robots-Tag header
- resp = manager_plone_client.get("/test-page")
- assert resp.status_code == 200
- assert resp.headers["X-Robots-Tag"] == "noindex"
-
- # Confirm views of the page are served with X-Robots-Tag header
- resp = manager_plone_client.get("/test-page/@@view")
- assert resp.status_code == 200
- assert resp.headers["X-Robots-Tag"] == "noindex"
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index bb98e04..0000000
--- a/tox.ini
+++ /dev/null
@@ -1,211 +0,0 @@
-# Generated from:
-# https://github.com/plone/meta/tree/master/config/default
-# See the inline comments on how to expand/tweak this configuration file
-[tox]
-# We need 4.4.0 for constrain_package_deps.
-min_version = 4.4.0
-envlist =
- lint
- test
- dependencies
-
-
-##
-# Add extra configuration options in .meta.toml:
-# [tox]
-# envlist_lines = """
-# my_other_environment
-# """
-# config_lines = """
-# my_extra_top_level_tox_configuration_lines
-# """
-##
-
-[testenv]
-skip_install = true
-allowlist_externals =
- echo
- false
-# Make sure typos like `tox -e formaat` are caught instead of silently doing nothing.
-# See https://github.com/tox-dev/tox/issues/2858.
-commands =
- echo "Unrecognized environment name {envname}"
- false
-
-[testenv:init]
-description = Prepare environment
-skip_install = true
-deps =
- mxdev
-commands =
- mxdev -c mx.ini
- echo "Initial setup for mxdev"
-
-
-[testenv:format]
-description = automatically reformat code
-skip_install = true
-deps =
- pre-commit
-commands =
- pre-commit run -a pyupgrade
- pre-commit run -a isort
- pre-commit run -a black
- pre-commit run -a zpretty
-
-[testenv:lint]
-description = run linters that will help improve the code style
-skip_install = true
-deps =
- pre-commit
-commands =
- pre-commit run -a
-
-[testenv:dependencies]
-description = check if the package defines all its dependencies
-skip_install = true
-deps =
- build
- z3c.dependencychecker==2.11
-commands =
- python -m build --sdist --no-isolation
- dependencychecker
-
-[testenv:dependencies-graph]
-description = generate a graph out of the dependencies of the package
-skip_install = false
-allowlist_externals =
- sh
-deps =
- pipdeptree==2.5.1
- graphviz # optional dependency of pipdeptree
-commands =
- sh -c 'pipdeptree --exclude setuptools,wheel,pipdeptree,zope.interface,zope.component --graph-output svg > dependencies.svg'
-
-[testenv:test]
-description = run the tests
-use_develop = true
-skip_install = false
-constrain_package_deps = true
-set_env =
- ROBOT_BROWSER=headlesschrome
-
-##
-# Specify extra test environment variables in .meta.toml:
-# [tox]
-# test_environment_variables = """
-# PIP_EXTRA_INDEX_URL=https://my-pypi.my-server.com/
-# """
-#
-# Set constrain_package_deps .meta.toml:
-# [tox]
-# constrain_package_deps = false
-##
-deps =
- pytest-plone
- pytest
- -c https://dist.plone.org/release/6.0-dev/constraints.txt
-
-##
-# Specify additional deps in .meta.toml:
-# [tox]
-# test_deps_additional = """
-# -esources/plonegovbr.portal_base[test]
-# """
-#
-# Specify a custom constraints file in .meta.toml:
-# [tox]
-# constraints_file = "https://my-server.com/constraints.txt"
-##
-commands =
- pytest --disable-warnings {posargs} {toxinidir}/tests
-extras =
- test
-
-
-[testenv:coverage]
-description = get a test coverage report
-use_develop = true
-skip_install = false
-constrain_package_deps = true
-set_env =
- ROBOT_BROWSER=headlesschrome
-
-##
-# Specify extra test environment variables in .meta.toml:
-# [tox]
-# test_environment_variables = """
-# PIP_EXTRA_INDEX_URL=https://my-pypi.my-server.com/
-# """
-#
-# Set constrain_package_deps .meta.toml:
-# [tox]
-# constrain_package_deps = "false"
-##
-deps =
- pytest-plone
- pytest
- coverage
- -c https://dist.plone.org/release/6.0-dev/constraints.txt
-
-commands =
- coverage run --source kitconcept.seo -m pytest {posargs} --disable-warnings {toxinidir}/tests
- coverage report -m --format markdown
- coverage xml
-extras =
- test
-
-
-[testenv:release-check]
-description = ensure that the package is ready to release
-skip_install = true
-deps =
- twine
- build
- towncrier
- -c https://dist.plone.org/release/6.0-dev/constraints.txt
-
-commands =
- # fake version to not have to install the package
- # we build the change log as news entries might break
- # the README that is displayed on PyPI
- towncrier build --version=100.0.0 --yes
- python -m build --sdist --no-isolation
- twine check dist/*
-
-[testenv:circular]
-description = ensure there are no cyclic dependencies
-use_develop = true
-skip_install = false
-set_env =
-
-##
-# Specify extra test environment variables in .meta.toml:
-# [tox]
-# test_environment_variables = """
-# PIP_EXTRA_INDEX_URL=https://my-pypi.my-server.com/
-# """
-##
-allowlist_externals =
- sh
-deps =
- pipdeptree
- pipforester
- -c https://dist.plone.org/release/6.0-dev/constraints.txt
-
-commands =
- # Generate the full dependency tree
- sh -c 'pipdeptree -j > forest.json'
- # Generate a DOT graph with the circular dependencies, if any
- pipforester -i forest.json -o forest.dot --cycles
- # Report if there are any circular dependencies, i.e. error if there are any
- pipforester -i forest.json --check-cycles -o /dev/null
-
-
-##
-# Add extra configuration options in .meta.toml:
-# [tox]
-# extra_lines = """
-# _your own configuration lines_
-# """
-##