summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-12-12 11:57:01 +0100
committerDaniel Baumann <daniel@debian.org>2024-12-12 11:57:01 +0100
commit8e29081b9d01c2e1177adb00224cc04ee4dd7642 (patch)
treec6b8ad3aab9eacab43fa63160cfbdf2adc1371b5
parentInitial commit. (diff)
downloadablog-8e29081b9d01c2e1177adb00224cc04ee4dd7642.tar.xz
ablog-8e29081b9d01c2e1177adb00224cc04ee4dd7642.zip
Adding upstream version 0.11.12.upstream/0.11.12upstream
Signed-off-by: Daniel Baumann <daniel@debian.org>
-rw-r--r--.codecov.yaml8
-rw-r--r--.djlintrc10
-rw-r--r--.github/dependabot.yml6
-rw-r--r--.github/workflows/ci.yml145
-rw-r--r--.gitignore198
-rw-r--r--.pre-commit-config.yaml54
-rw-r--r--.readthedocs.yml21
-rw-r--r--.rtd-environment.yml10
-rw-r--r--.stylelintrc.json3
-rw-r--r--LICENSE.rst21
-rw-r--r--Makefile28
-rw-r--r--README.rst26
-rw-r--r--ablog-conda-test-env.yml23
-rw-r--r--conftest.py30
-rw-r--r--docs/Makefile19
-rw-r--r--docs/_static/ablog.icobin0 -> 16958 bytes
-rw-r--r--docs/_static/ablog.pngbin0 -> 3993 bytes
-rw-r--r--docs/_static/ablog.svg949
-rw-r--r--docs/conf.py151
-rw-r--r--docs/index.rst104
-rw-r--r--docs/make.bat35
-rw-r--r--docs/manual/ablog-commands.rst178
-rw-r--r--docs/manual/ablog-configuration-options.rst291
-rw-r--r--docs/manual/ablog-i18n.rst56
-rw-r--r--docs/manual/ablog-quick-start.rst126
-rw-r--r--docs/manual/api.rst20
-rw-r--r--docs/manual/cross-referencing-blog-pages.rst47
-rw-r--r--docs/manual/deploy-to-github-pages.rst41
-rw-r--r--docs/manual/external-post.rst10
-rw-r--r--docs/manual/forever-draft.rst33
-rw-r--r--docs/manual/images/notebook_cells.pngbin0 -> 13333 bytes
-rw-r--r--docs/manual/markdown.md56
-rw-r--r--docs/manual/notebook_support.ipynb81
-rw-r--r--docs/manual/post-excerpts-and-images.rst44
-rw-r--r--docs/manual/posting-and-listing.rst212
-rw-r--r--docs/manual/templates-themes.rst101
-rw-r--r--docs/manual/watch-yourself-blogging.rst21
-rw-r--r--docs/nitpick-exceptions8
-rw-r--r--docs/release/ablog-v0.1-released.rst20
-rw-r--r--docs/release/ablog-v0.10-released.rst282
-rw-r--r--docs/release/ablog-v0.11-released.rst128
-rw-r--r--docs/release/ablog-v0.2-released.rst42
-rw-r--r--docs/release/ablog-v0.3-released.rst28
-rw-r--r--docs/release/ablog-v0.4-released.rst33
-rw-r--r--docs/release/ablog-v0.5-released.rst15
-rw-r--r--docs/release/ablog-v0.6-released.rst46
-rw-r--r--docs/release/ablog-v0.7-released.rst92
-rw-r--r--docs/release/ablog-v0.8-released.rst58
-rw-r--r--docs/release/ablog-v0.9-released.rst61
-rw-r--r--pyproject.toml64
-rw-r--r--roots/test-build/conf.py16
-rw-r--r--roots/test-build/foo-empty-post.rst5
-rw-r--r--roots/test-build/index.rst2
-rw-r--r--roots/test-build/post.rst11
-rw-r--r--roots/test-canonical/canonical.rst9
-rw-r--r--roots/test-canonical/conf.py18
-rw-r--r--roots/test-canonical/index.rst2
-rw-r--r--roots/test-canonical/post.rst11
-rw-r--r--roots/test-canonical/postlist.rst4
-rw-r--r--roots/test-external/conf.py16
-rw-r--r--roots/test-external/external.rst8
-rw-r--r--roots/test-external/index.rst2
-rw-r--r--roots/test-external/postlist.rst4
-rw-r--r--roots/test-parallel/conf.py1
-rw-r--r--roots/test-parallel/index.rst7
-rw-r--r--roots/test-parallel/post1.rst4
-rw-r--r--roots/test-parallel/post2.rst4
-rw-r--r--roots/test-parallel/post3.rst4
-rw-r--r--roots/test-parallel/post4.rst4
-rw-r--r--roots/test-parallel/postlist.rst4
-rw-r--r--roots/test-postlist/conf.py1
-rw-r--r--roots/test-postlist/index.rst7
-rw-r--r--roots/test-postlist/post.rst4
-rw-r--r--roots/test-postlist/postlist.rst4
-rw-r--r--roots/test-templates/_templates/ablog/postcard.html1
-rw-r--r--roots/test-templates/_themes/test_theme/ablog/postcard.html1
-rw-r--r--roots/test-templates/_themes/test_theme/theme.toml5
-rw-r--r--roots/test-templates/conf.py1
-rw-r--r--roots/test-templates/index.rst7
-rw-r--r--roots/test-templates/post.rst5
-rw-r--r--roots/test-templates/postlist.rst4
-rw-r--r--setup.cfg112
-rw-r--r--setup.py30
-rwxr-xr-xsrc/ablog/__init__.py172
-rw-r--r--src/ablog/blog.py615
-rw-r--r--src/ablog/commands.py469
-rw-r--r--src/ablog/locales/ca/LC_MESSAGES/sphinx.mobin0 -> 1463 bytes
-rw-r--r--src/ablog/locales/ca/LC_MESSAGES/sphinx.po129
-rw-r--r--src/ablog/locales/de/LC_MESSAGES/sphinx.mobin0 -> 1271 bytes
-rw-r--r--src/ablog/locales/de/LC_MESSAGES/sphinx.po131
-rw-r--r--src/ablog/locales/es/LC_MESSAGES/sphinx.mobin0 -> 1291 bytes
-rw-r--r--src/ablog/locales/es/LC_MESSAGES/sphinx.po131
-rw-r--r--src/ablog/locales/et/LC_MESSAGES/sphinx.mobin0 -> 1212 bytes
-rw-r--r--src/ablog/locales/et/LC_MESSAGES/sphinx.po131
-rw-r--r--src/ablog/locales/fr/LC_MESSAGES/sphinx.mobin0 -> 1313 bytes
-rw-r--r--src/ablog/locales/fr/LC_MESSAGES/sphinx.po130
-rw-r--r--src/ablog/locales/it/LC_MESSAGES/sphinx.mobin0 -> 1524 bytes
-rw-r--r--src/ablog/locales/it/LC_MESSAGES/sphinx.po130
-rw-r--r--src/ablog/locales/pt/LC_MESSAGES/sphinx.mobin0 -> 1435 bytes
-rw-r--r--src/ablog/locales/pt/LC_MESSAGES/sphinx.po129
-rw-r--r--src/ablog/locales/ru/LC_MESSAGES/sphinx.mobin0 -> 1467 bytes
-rw-r--r--src/ablog/locales/ru/LC_MESSAGES/sphinx.po132
-rw-r--r--src/ablog/locales/sphinx.pot128
-rw-r--r--src/ablog/locales/tr/LC_MESSAGES/sphinx.mobin0 -> 1217 bytes
-rw-r--r--src/ablog/locales/tr/LC_MESSAGES/sphinx.po131
-rw-r--r--src/ablog/locales/zh_CN/LC_MESSAGES/sphinx.mobin0 -> 1315 bytes
-rw-r--r--src/ablog/locales/zh_CN/LC_MESSAGES/sphinx.po130
-rw-r--r--src/ablog/post.py736
-rw-r--r--src/ablog/start.py617
-rw-r--r--src/ablog/stylesheets/ablog/tagcloud.css36
-rw-r--r--src/ablog/templates/ablog/archives.html14
-rw-r--r--src/ablog/templates/ablog/authors.html14
-rw-r--r--src/ablog/templates/ablog/catalog.html25
-rw-r--r--src/ablog/templates/ablog/categories.html14
-rw-r--r--src/ablog/templates/ablog/collection.html57
-rw-r--r--src/ablog/templates/ablog/languages.html14
-rw-r--r--src/ablog/templates/ablog/locations.html14
-rw-r--r--src/ablog/templates/ablog/postcard.html21
-rw-r--r--src/ablog/templates/ablog/postcard2.html105
-rw-r--r--src/ablog/templates/ablog/postnavy.html28
-rw-r--r--src/ablog/templates/ablog/recentposts.html17
-rw-r--r--src/ablog/templates/ablog/redirect.html8
-rw-r--r--src/ablog/templates/ablog/tagcloud.html17
-rw-r--r--src/ablog/templates/archives.html15
-rw-r--r--src/ablog/templates/authors.html15
-rw-r--r--src/ablog/templates/catalog.html26
-rw-r--r--src/ablog/templates/categories.html15
-rw-r--r--src/ablog/templates/collection.html59
-rw-r--r--src/ablog/templates/languages.html15
-rw-r--r--src/ablog/templates/locations.html15
-rw-r--r--src/ablog/templates/page.html56
-rw-r--r--src/ablog/templates/postcard.html22
-rw-r--r--src/ablog/templates/postcard2.html107
-rw-r--r--src/ablog/templates/postnavy.html30
-rw-r--r--src/ablog/templates/recentposts.html18
-rw-r--r--src/ablog/templates/redirect.html10
-rw-r--r--src/ablog/templates/tagcloud.html18
-rw-r--r--src/ablog/tests/__init__.py0
-rw-r--r--src/ablog/tests/test_build.py84
-rw-r--r--src/ablog/tests/test_canonical.py25
-rw-r--r--src/ablog/tests/test_external.py32
-rw-r--r--src/ablog/tests/test_parallel.py34
-rw-r--r--src/ablog/tests/test_postlist.py38
-rw-r--r--src/ablog/tests/test_templates.py82
-rw-r--r--src/ablog/version.py17
-rw-r--r--tox.ini42
146 files changed, 9553 insertions, 0 deletions
diff --git a/.codecov.yaml b/.codecov.yaml
new file mode 100644
index 0000000..12e27d0
--- /dev/null
+++ b/.codecov.yaml
@@ -0,0 +1,8 @@
+codecov:
+ token: a0dfd87f-8eb9-4a41-9e4e-a06919f216cd
+comment: off
+coverage:
+ status:
+ project:
+ default:
+ threshold: 0.2%
diff --git a/.djlintrc b/.djlintrc
new file mode 100644
index 0000000..0439693
--- /dev/null
+++ b/.djlintrc
@@ -0,0 +1,10 @@
+{
+ "profile": "jinja",
+ "extension": "html",
+ "indent": "2",
+ "max_line_length": "120",
+ "use_gitignore": "True",
+ "format_js": "True",
+ "format_css": "True",
+ "ignore": "H006,J018,T003,H025"
+}
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..8ac6b8c
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..5d6d588
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,145 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - "main"
+ - "*.*"
+ - "!*backport*"
+ tags:
+ - "v*"
+ - "!*dev*"
+ - "!*pre*"
+ - "!*post*"
+ pull_request:
+ # Allow manual runs through the web UI
+ workflow_dispatch:
+ schedule:
+ # ┌───────── minute (0 - 59)
+ # │ ┌───────── hour (0 - 23)
+ # │ │ ┌───────── day of the month (1 - 31)
+ # │ │ │ ┌───────── month (1 - 12 or JAN-DEC)
+ # │ │ │ │ ┌───────── day of the week (0 - 6 or SUN-SAT)
+ - cron: "0 7 * * *" # Every day at 07:00 UTC
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ core:
+ uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@main
+ with:
+ submodules: false
+ coverage: codecov
+ libraries: |
+ apt:
+ - pandoc
+ - graphviz
+ envs: |
+ - linux: py313-sphinx8
+ secrets:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+
+ test:
+ needs: [core]
+ uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@main
+ with:
+ submodules: false
+ libraries: |
+ brew:
+ - pandoc
+ - graphviz
+ choco:
+ - pandoc
+ - graphviz
+ apt:
+ - pandoc
+ - graphviz
+ envs: |
+ - macos: py312-sphinx8
+ - windows: py311-sphinx8
+ - linux: py310-sphinx8
+ - linux: py312-pydata-sphinx-theme
+ - linux: py313-devdeps
+
+ extra_tests:
+ needs: [test]
+ uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@main
+ with:
+ submodules: false
+ libraries: |
+ apt:
+ - pandoc
+ - graphviz
+ envs: |
+ - linux: py312-pydata-sphinx-theme
+ - linux: py313-devdeps
+
+ docs:
+ needs: [core]
+ uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@main
+ with:
+ submodules: false
+ pytest: false
+ libraries: |
+ apt:
+ - pandoc
+ - graphviz
+ envs: |
+ - linux: py313-docs
+ - linux: py313-linkcheck
+
+ sdist_verify:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.13"
+ - run: python -m pip install -U --user build
+ - run: python -m build . --sdist
+ - run: python -m pip install -U --user twine
+ - run: python -m twine check dist/*
+
+ conda:
+ needs: [test]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ lfs: true
+ - uses: conda-incubator/setup-miniconda@v3
+ with:
+ activate-environment: ablog-test
+ environment-file: ablog-conda-test-env.yml
+ python-version: "3.13"
+ - name: Install ablog
+ shell: bash -el {0}
+ run: |
+ pip install --no-deps --no-build-isolation .
+ - name: Run test
+ shell: bash -el {0}
+ run: |
+ conda list
+ cd /tmp
+ pytest -vvv -r a --pyargs ablog
+ make tests
+
+ publish:
+ # Build wheels on PRs only when labelled. Releases will only be published if tagged ^v.*
+ # see https://github-actions-workflows.openastronomy.org/en/latest/publish.html#upload-to-pypi
+ if: |
+ github.event_name != 'pull_request' ||
+ (
+ github.event_name == 'pull_request' &&
+ contains(github.event.pull_request.labels.*.name, 'Run publish')
+ )
+ needs: [test, docs, sdist_verify]
+ uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish_pure_python.yml@main
+ with:
+ python-version: "3.13"
+ submodules: false
+ secrets:
+ pypi_token: ${{ secrets.PYPI_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b68d972
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,198 @@
+### Python: https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+pip-wheel-metadata/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+junit/
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+
+### https://raw.github.com/github/gitignore/master/Global/OSX.gitignore
+
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must ends with two \r.
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear on external disk
+.Spotlight-V100
+.Trashes
+
+### Linux: https://raw.githubusercontent.com/github/gitignore/master/Global/Linux.gitignore
+
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### MacOS: https://raw.githubusercontent.com/github/gitignore/master/Global/macOS.gitignore
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.TemporaryItems
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Windows: https://raw.githubusercontent.com/github/gitignore/master/Global/Windows.gitignore
+
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+### VScode: https://raw.githubusercontent.com/github/gitignore/master/Global/VisualStudioCode.gitignore
+.vscode/*
+
+### Pycharm(?)
+.idea
+
+# Ablog
+.github_cache
+src/ablog/version.py
+docs/_build/
+docs/api/
+docs/.doctrees/
+docs/_website/
+docs/_latex/
+test/
+*.orig
+.history/
+pydata-sphinx-theme/
+_build
+demo/
+src/ablog/_version.py
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..a1dc532
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,54 @@
+ci:
+ autofix_prs: false
+ autoupdate_schedule: "quarterly"
+repos:
+ - repo: https://github.com/PyCQA/docformatter
+ rev: eb1df347edd128b30cd3368dddc3aa65edcfac38
+ hooks:
+ - id: docformatter
+ args: ["--in-place", "--pre-summary-newline", "--make-summary-multi"]
+ - repo: https://github.com/PyCQA/autoflake
+ rev: v2.3.1
+ hooks:
+ - id: autoflake
+ args:
+ [
+ "--in-place",
+ "--remove-all-unused-imports",
+ "--remove-unused-variable",
+ ]
+ exclude: ".*(.fits|.fts|.fit|.txt|tca.*|extern.*|.rst|.md|docs/conf.py)$"
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: "v0.7.2"
+ hooks:
+ - id: ruff
+ args: ["--fix", "--unsafe-fixes"]
+ - id: ruff-format
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v5.0.0
+ hooks:
+ - id: check-ast
+ - id: check-case-conflict
+ - id: trailing-whitespace
+ exclude: ".*(.fits|.fts|.fit|.txt|.csv)$"
+ - id: mixed-line-ending
+ exclude: ".*(.fits|.fts|.fit|.txt|.csv)$"
+ - id: end-of-file-fixer
+ exclude: ".*(.fits|.fts|.fit|.txt|.csv)$"
+ - id: check-yaml
+ - id: debug-statements
+ - repo: https://github.com/codespell-project/codespell
+ rev: v2.3.0
+ hooks:
+ - id: codespell
+ additional_dependencies:
+ - tomli
+ - repo: https://github.com/rbubley/mirrors-prettier
+ rev: v3.3.3
+ hooks:
+ - id: prettier
+ - repo: https://github.com/Riverside-Healthcare/djLint
+ rev: v1.35.4
+ hooks:
+ - id: djlint-jinja
+ types_or: ["html"]
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..ba5a3a3
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,21 @@
+version: 2
+build:
+ os: ubuntu-lts-latest
+ tools:
+ python: "mambaforge-latest"
+ jobs:
+ pre_install:
+ - git update-index --assume-unchanged .rtd-environment.yml docs/conf.py
+conda:
+ environment: .rtd-environment.yml
+sphinx:
+ builder: html
+ configuration: docs/conf.py
+ fail_on_warning: false
+python:
+ install:
+ - method: pip
+ extra_requirements:
+ - all
+ - docs
+ path: .
diff --git a/.rtd-environment.yml b/.rtd-environment.yml
new file mode 100644
index 0000000..f2c4dd2
--- /dev/null
+++ b/.rtd-environment.yml
@@ -0,0 +1,10 @@
+name: rtd_ablog
+channels:
+ - conda-forge
+dependencies:
+ - python=3.12
+ - pip
+ - graphviz
+ - make
+ - nbsphinx
+ - pandoc
diff --git a/.stylelintrc.json b/.stylelintrc.json
new file mode 100644
index 0000000..40db42c
--- /dev/null
+++ b/.stylelintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "stylelint-config-standard"
+}
diff --git a/LICENSE.rst b/LICENSE.rst
new file mode 100644
index 0000000..251badd
--- /dev/null
+++ b/LICENSE.rst
@@ -0,0 +1,21 @@
+ABlog for blogging with Sphinx
+
+Copyright (C) 2014-2022 Ahmet Bakan and The SunPy Developers
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..b8ddd5f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,28 @@
+.PHONY: demo rebuild tests
+
+demo:
+ rm -rf demo && mkdir demo
+ printf "demo\nABlog\nABlog Team\nhttps://ablog.readthedocs.io/" | ablog start
+
+rebuild:
+ cd docs; watchmedo shell-command --patterns='*.rst' --command='ablog build' --recursive
+
+test:
+ set -e; cd docs; git clean -xfd; ablog build -T -W; git clean -xfd; cd ..
+
+test1:
+ set -e; cd docs; git clean -xfd; ablog build -T -W -b json; git clean -xfd; cd ..
+
+test2:
+ set -e; cd docs; git clean -xfd; ablog build -T -W -b pickle; git clean -xfd; cd ..
+
+test3:
+ set -e; mkdir -p test; cd test; git clean -xfd; printf "\nABlog\nABlog Team\nhttps://ablog.readthedocs.io/" | ablog start; ablog build -W; cd ..; rm -rf test
+
+test4:
+ set -e; mkdir -p testablog; cd testablog; git clean -xfd; printf "\nABlog\nABlog Team\nhttps://ablog.readthedocs.io/" | ablog start; ablog build -W; cd ..; rm -rf testablog
+
+test5:
+ set -e; cd docs; git clean -xfd; ablog build -W -b latex -T -d .doctrees -w _latex; git clean -xfd; cd ..
+
+tests: test test1 test2 test3 test4 test5
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..ba29ffa
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,26 @@
+ABlog for Sphinx
+================
+
+|CI| |Upload Python Package|
+
+.. |CI| image:: https://github.com/sunpy/ablog/actions/workflows/ci.yml/badge.svg
+ :target: https://github.com/sunpy/ablog/actions/workflows/ci.yml
+.. |Upload Python Package| image:: https://github.com/sunpy/ablog/actions/workflows/pythonpublish.yml/badge.svg
+ :target: https://github.com/sunpy/ablog/actions/workflows/pythonpublish.yml
+
+ABlog is a Sphinx extension that converts any documentation or personal website project into a full-fledged blog.
+
+`Please check our documentation for information on how to get started. <https://ablog.readthedocs.io/>`__
+
+Note
+----
+
+This is the new home of `Ahmet Bakan's Ablog Sphinx extension <https://github.com/abakan-zz/ablog/>`__.
+The original project is no longer maintained and the `SunPy Project <https://www.sunpy.org>`__ has taken over maintenance.
+
+Warning
+-------
+
+**This version is maintained with the aim to keep it working for SunPy Project website and thus new features or bugfixes are highly unlikely unless they directly impact the SunPy Project**
+
+**We strongly encourage users and interested in parties in submitting patches to ablog**
diff --git a/ablog-conda-test-env.yml b/ablog-conda-test-env.yml
new file mode 100644
index 0000000..d38d40a
--- /dev/null
+++ b/ablog-conda-test-env.yml
@@ -0,0 +1,23 @@
+name: ablog-conda-test-env
+channels:
+ - conda-forge
+dependencies:
+ - docutils
+ - feedgen
+ - graphviz
+ - invoke
+ - make
+ - myst-parser
+ - nbsphinx
+ - packaging
+ - pandoc
+ - pip
+ - pytest
+ - python-dateutil
+ - setuptools
+ - setuptools-scm
+ - sphinx
+ - sphinx-automodapi
+ - watchdog
+ - pip:
+ - sunpy-sphinx-theme
diff --git a/conftest.py b/conftest.py
new file mode 100644
index 0000000..8a58b65
--- /dev/null
+++ b/conftest.py
@@ -0,0 +1,30 @@
+from pathlib import Path
+
+import docutils
+import pytest
+import sphinx
+
+# Load app, status and warning fixtures.
+pytest_plugins = ["sphinx.testing.fixtures"]
+
+
+# inspired from sphinx's conftest.py
+def pytest_report_header(config):
+ header = f"libraries: Sphinx-{sphinx.__display_version__}, docutils-{docutils.__version__}"
+ if hasattr(config, "_tmp_path_factory"):
+ header += f"\nbase tempdir: {config._tmp_path_factory.getbasetemp()}"
+
+ return header
+
+
+@pytest.fixture(scope="session")
+def rootdir():
+ return Path(__file__).parent.absolute() / "roots"
+
+
+@pytest.fixture(scope="function", autouse=True)
+def reset_blog_config():
+ # Reset cached configurations to enable confoverrides
+ from ablog.blog import Blog
+
+ Blog._dict = {}
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..e602d24
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,19 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS = -v
+SPHINXBUILD = sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/_static/ablog.ico b/docs/_static/ablog.ico
new file mode 100644
index 0000000..31b1671
--- /dev/null
+++ b/docs/_static/ablog.ico
Binary files differ
diff --git a/docs/_static/ablog.png b/docs/_static/ablog.png
new file mode 100644
index 0000000..fc21bf3
--- /dev/null
+++ b/docs/_static/ablog.png
Binary files differ
diff --git a/docs/_static/ablog.svg b/docs/_static/ablog.svg
new file mode 100644
index 0000000..aa80a42
--- /dev/null
+++ b/docs/_static/ablog.svg
@@ -0,0 +1,949 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="https://purl.org/dc/elements/1.1/"
+ xmlns:cc="https://creativecommons.org/ns#"
+ xmlns:rdf="https://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="https://www.w3.org/2000/svg"
+ xmlns="https://www.w3.org/2000/svg"
+ xmlns:sodipodi="https://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="https://www.inkscape.org/namespaces/inkscape"
+ height="1200"
+ width="3600"
+ id="svg4026"
+ version="1.1"
+ inkscape:version="0.48.2 r9819"
+ sodipodi:docname="ablog.svg"
+ inkscape:export-filename="/Users/abakan/Documents/GitHub/ablog/docs/_static/ablog.png"
+ inkscape:export-xdpi="5.0250001"
+ inkscape:export-ydpi="5.0250001">
+ <metadata
+ id="metadata4034">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="https://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs4032">
+ <filter
+ id="filter3248"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur3250"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend3252"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence3254"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap3256"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology3258"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend3260"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter3262"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur3264"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend3266"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence3268"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap3270"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology3272"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend3274"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter3385"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur3387"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend3389"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence3391"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap3393"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology3395"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend3397"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter3399"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur3401"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend3403"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence3405"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap3407"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology3409"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend3411"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter3413"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur3415"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend3417"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence3419"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap3421"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology3423"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend3425"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter3427"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur3429"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend3431"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence3433"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap3435"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology3437"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend3439"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter3441"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur3443"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend3445"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence3447"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap3449"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology3451"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend3453"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter3455"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur3457"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend3459"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence3461"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap3463"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology3465"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend3467"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter3950"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur3952"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend3954"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence3956"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap3958"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology3960"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend3962"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter3964"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur3966"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend3968"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence3970"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap3972"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology3974"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend3976"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter3978"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur3980"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend3982"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence3984"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap3986"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology3988"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend3990"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter3992"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur3994"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend3996"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence3998"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap4000"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology4002"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend4004"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter4006"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur4008"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend4010"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence4012"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap4014"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology4016"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend4018"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter4020"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur4022"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend4024"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence4026"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap4028"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology4030"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend4032"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter4397"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur4399"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend4401"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence4403"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap4405"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology4407"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend4409"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter4411"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur4413"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend4415"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence4417"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap4419"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology4421"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend4423"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter4425"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur4427"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend4429"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence4431"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap4433"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology4435"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend4437"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter4439"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur4441"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend4443"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence4445"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap4447"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology4449"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend4451"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter4453"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur4455"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend4457"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence4459"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap4461"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology4463"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend4465"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ <filter
+ id="filter4467"
+ inkscape:label="Air spray"
+ inkscape:menu="Scatter"
+ inkscape:menu-tooltip="Convert to small scattered particles with some thickness"
+ width="2"
+ height="2"
+ y="-0.5"
+ x="-0.5"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ id="feGaussianBlur4469"
+ stdDeviation="0.01"
+ result="result1" />
+ <feBlend
+ id="feBlend4471"
+ in2="result1"
+ result="fbSourceGraphic"
+ mode="multiply" />
+ <feTurbulence
+ id="feTurbulence4473"
+ baseFrequency="0.8"
+ type="fractalNoise"
+ seed="0"
+ numOctaves="3"
+ result="result3" />
+ <feDisplacementMap
+ id="feDisplacementMap4475"
+ in2="result3"
+ in="fbSourceGraphic"
+ xChannelSelector="R"
+ yChannelSelector="G"
+ scale="50"
+ result="result2" />
+ <feMorphology
+ id="feMorphology4477"
+ radius="1"
+ operator="dilate"
+ result="result4" />
+ <feBlend
+ id="feBlend4479"
+ in2="result2"
+ mode="screen" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1286"
+ inkscape:window-height="1009"
+ id="namedview4030"
+ showgrid="false"
+ inkscape:zoom="0.236"
+ inkscape:cx="2155.7813"
+ inkscape:cy="500"
+ inkscape:window-x="401"
+ inkscape:window-y="175"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg4026" />
+ <text
+ xml:space="preserve"
+ style="font-size:1160px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:Arial Rounded MT Bold;-inkscape-font-specification:'Arial Rounded MT Bold,'"
+ x="944.91528"
+ y="907.62714"
+ id="text3784"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan3786"
+ x="944.91528"
+ y="907.62714"
+ style="font-size:1160px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;font-family:Arial Rounded MT Bold;-inkscape-font-specification:'Arial Rounded MT Bold,'">Blog</tspan></text>
+ <path
+ d="M 833.65625 83.84375 C 701.98958 83.84375 580.11458 116.55208 468.03125 181.96875 C 355.94792 247.38542 267.40625 336.13542 202.40625 448.21875 C 137.40625 560.30208 104.90625 682.17708 104.90625 813.84375 C 104.90625 843.01042 115.11458 867.59375 135.53125 887.59375 C 155.94792 907.59375 180.32292 917.59375 208.65625 917.59375 C 236.98958 917.59375 261.36458 907.59375 281.78125 887.59375 C 302.19792 867.59375 312.40625 843.01042 312.40625 813.84375 C 312.40625 669.67708 363.44792 546.76042 465.53125 445.09375 C 567.61458 343.42708 690.32292 292.59375 833.65625 292.59375 C 861.98958 292.59375 886.36458 282.59375 906.78125 262.59375 C 927.19792 242.59375 937.40625 218.01042 937.40625 188.84375 C 937.40625 159.67708 927.19792 134.88542 906.78125 114.46875 C 886.36458 94.052083 861.98958 83.84375 833.65625 83.84375 z M 833.65625 396.375 C 753.11275 396.375 699.43885 415.49755 674.84375 425.96875 C 652.66285 435.41205 632.55455 445.84635 604.28125 465.34375 C 577.68735 483.68305 558.5651 513.6848 559.625 547.0625 C 560.5463 576.0772 562.6609 593.9731 583.6875 617.625 C 611.3285 648.7173 626.15755 648.26095 641.28125 652.46875 C 667.09175 659.64985 699.51095 650.3316 716.21875 640.5625 C 727.78705 633.7985 737.68565 628.10535 746.46875 623.84375 L 746.46875 808.4375 C 746.39575 810.2263 746.375 812.0521 746.375 813.875 C 746.375 843.0417 755.7673 867.625 774.5625 887.625 C 793.3577 907.625 815.7919 917.625 841.875 917.625 C 867.9582 917.625 890.3923 907.625 909.1875 887.625 C 927.9827 867.625 937.40625 843.0417 937.40625 813.875 C 937.40625 813.3721 937.38 812.8754 937.375 812.375 L 937.46875 812.375 L 937.46875 509.75 L 937.09375 509.75 C 937.28755 506.9953 937.40625 504.2168 937.40625 501.375 C 937.40625 472.2084 927.19785 447.4167 906.78125 427 C 886.36455 406.5834 861.98955 396.375 833.65625 396.375 z M 521 709.09375 C 492.66667 709.09375 468.29167 719.30208 447.875 739.71875 C 427.45833 760.13542 417.25 784.92708 417.25 814.09375 C 417.25 843.26042 427.45833 867.84375 447.875 887.84375 C 468.29167 907.84375 492.66667 917.84375 521 917.84375 C 549.33333 917.84375 573.70833 907.84375 594.125 887.84375 C 614.54167 867.84375 624.75 843.26042 624.75 814.09375 C 624.75 784.92708 614.54167 760.13542 594.125 739.71875 C 573.70833 719.30208 549.33333 709.09375 521 709.09375 z "
+ id="path3094-3" />
+</svg>
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..8c88230
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,151 @@
+import re
+from pathlib import Path
+
+from packaging.version import parse as _parse
+from sphinx import addnodes
+
+import ablog
+
+ablog_builder = "dirhtml"
+ablog_website = "_website"
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.doctest",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.todo",
+ "sphinx.ext.ifconfig",
+ "sphinx.ext.extlinks",
+ "sphinx_automodapi.automodapi",
+ "ablog",
+ "alabaster",
+ "nbsphinx",
+ "myst_parser",
+]
+
+version = str(_parse(ablog.__version__))
+project = "ABlog"
+copyright = "2014-2022, ABlog Team"
+master_doc = "index"
+source_suffix = {
+ ".rst": "restructuredtext",
+ ".md": "markdown",
+}
+exclude_patterns = ["_build", "docs/manual/.ipynb_checkpoints"]
+html_title = "ABlog"
+html_use_index = True
+html_domain_indices = False
+html_show_sourcelink = True
+html_favicon = "_static/ablog.ico"
+blog_title = "ABlog"
+blog_baseurl = "https://ablog.readthedocs.io/"
+blog_locations = {
+ "Pittsburgh": ("Pittsburgh, PA", "https://en.wikipedia.org/wiki/Pittsburgh"),
+ "San Fran": ("San Francisco, CA", "https://en.wikipedia.org/wiki/San_Francisco"),
+ "Denizli": ("Denizli, Turkey", "https://en.wikipedia.org/wiki/Denizli"),
+}
+blog_languages = {
+ "en": ("English", None),
+ "nl": ("Nederlands", None),
+ "zh_CN": ("Chinese", None),
+}
+blog_default_language = "en"
+language = "en"
+blog_authors = {
+ "Ahmet": ("Ahmet Bakan", "https://ahmetbakan.com"),
+ "Luc": ("Luc Saffre", "https://saffre-rumma.net/luc/"),
+ "Mehmet": ("Mehmet Gerçeker", "https://github.com/mehmetg"),
+ "Libor": ("Libor Jelínek", "https://liborjelinek.github.io/"),
+}
+blog_feed_archives = True
+blog_feed_fulltext = True
+blog_feed_templates = {
+ "atom": {
+ "content": "{{ title }}{% for tag in post.tags %}" " #{{ tag.name|trim()|replace(' ', '') }}" "{% endfor %}",
+ },
+ "social": {
+ "content": "{{ title }}{% for tag in post.tags %}" " #{{ tag.name|trim()|replace(' ', '') }}" "{% endfor %}",
+ },
+}
+disqus_shortname = "https-ablog-readthedocs-io"
+disqus_pages = True
+fontawesome_link_cdn = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css"
+html_theme = "alabaster"
+html_sidebars = {
+ "**": [
+ "about.html", # Comes from alabaster
+ "searchfield.html", # Comes from alabaster
+ "ablog/postcard.html",
+ "ablog/recentposts.html",
+ "ablog/tagcloud.html",
+ "ablog/categories.html",
+ "ablog/archives.html",
+ "ablog/authors.html",
+ "ablog/languages.html",
+ "ablog/locations.html",
+ ]
+}
+html_theme_options = {
+ "travis_button": False,
+ "github_user": "sunpy",
+ "github_repo": "ablog",
+ "description": "ABlog for blogging with Sphinx",
+ "logo": "ablog.png",
+}
+intersphinx_mapping = {
+ "python": ("https://docs.python.org/", None),
+ "sphinx": ("https://www.sphinx-doc.org/en/master/", None),
+}
+extlinks = {
+ "wiki": ("https://en.wikipedia.org/wiki/%s", "%s"),
+ "issue": ("https://github.com/sunpy/ablog/issues/%s", "issue %s"),
+ "pull": ("https://github.com/sunpy/ablog/pull/%s", "pull request %s"),
+}
+rst_epilog = """
+.. _Sphinx: http://sphinx-doc.org/
+.. _Python: https://python.org
+.. _Disqus: https://disqus.com/
+.. _GitHub: https://github.com/sunpy/ablog
+.. _PyPI: https://pypi.python.org/pypi/ablog
+.. _Read The Docs: https://readthedocs.org/
+.. _Alabaster: https://github.com/bitprophet/alabaster
+"""
+locale_dirs = [str(Path(ablog.__file__).parent / Path("locales"))]
+nitpicky = True
+nitpick_ignore = []
+for line in open("nitpick-exceptions"):
+ if line.strip() == "" or line.startswith("#"):
+ continue
+ dtype, target = line.split(None, 1)
+ target = target.strip()
+ nitpick_ignore.append((dtype, target))
+
+
+def parse_event(env, sig, signode):
+ event_sig_re = re.compile(r"([a-zA-Z-]+)\s*\((.*)\)")
+ m = event_sig_re.match(sig)
+ if not m:
+ signode += addnodes.desc_name(sig, sig)
+ return sig
+ name, args = m.groups()
+ signode += addnodes.desc_name(name, name)
+ plist = addnodes.desc_parameterlist()
+ for arg in args.split(","):
+ arg = arg.strip()
+ plist += addnodes.desc_parameter(arg, arg)
+ signode += plist
+ return name
+
+
+def setup(app):
+ from sphinx.ext.autodoc import cut_lines
+ from sphinx.util.docfields import GroupedField
+
+ app.connect("autodoc-process-docstring", cut_lines(4, what=["module"]))
+ app.add_object_type(
+ "confval",
+ "confval",
+ objname="configuration value",
+ indextemplate="pair: %s; configuration value",
+ )
+ fdesc = GroupedField("parameter", label="Parameters", names=["param"], can_collapse=True)
+ app.add_object_type("event", "event", "pair: %s; event", parse_event, doc_field_types=[fdesc])
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..407451f
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,104 @@
+ABlog for Sphinx
+================
+
+ABlog is a Sphinx extension that converts any documentation or personal website project into a full-fledged blog with:
+
+ * :ref:`Atom feeds <blog-feed>`
+ * :ref:`Archive pages <blog-archives>`
+ * :ref:`sidebars`
+ * :ref:`disqus-integration`
+ * :ref:`Font-Awesome integration <font-awesome>`
+ * :doc:`manual/markdown`
+
+Ablog is part of the `SunPy Project <https://www.sunpy.org>`__.
+
+.. _installation:
+
+Installation
+------------
+
+You can install ABlog using `pip <https://pip.pypa.io/en/stable/>`__::
+
+ pip install -U ablog
+
+or `miniforge <https://github.com/conda-forge/miniforge>`__::
+
+ conda install ablog
+
+This will also install `Sphinx <http://sphinx-doc.org/>`__, `feedgen <https://github.com/lkiesow/python-feedgen>`__, and `Invoke <https://www.pyinvoke.org/>`__ respectively required for building your website, making it look good, generating feeds, and running deploy commands.
+
+Getting Started
+---------------
+
+If you are starting a new project, see the :ref:`quick-start` guide.
+If you already have a project, enable blogging by making following changes in ``conf.py``:
+
+.. code-block:: python
+
+ # 1. Add 'ablog' and 'sphinx.ext.intersphinx' to the list of extensions
+ extensions = [
+ '...',
+ 'ablog',
+ 'sphinx.ext.intersphinx',
+ ]
+
+How it works
+------------
+
+If you are new to Sphinx_ and reStructuredText markup language, you might find `reStructuredText Primer`_ useful.
+Once you have content (in ``.rst`` files), you can post *any page* using the :rst:dir:`post` directive as follows:
+
+.. _reStructuredText Primer: https://www.sphinx-doc.org/en/master/
+
+.. code-block:: rst
+
+ .. post:: Apr 15, 2014
+ :tags: earth, love, peace
+ :category: python
+ :author: me
+ :location: SF
+ :language: en
+
+An alternative method is:
+
+.. code-block:: rst
+
+ :blogpost: true
+ :date: Oct 10, 2020
+ :author: Nabil Freij
+ :location: World
+ :category: Manual
+ :language: English
+
+at the top of the file.
+
+ABlog will index all files posted as above and list them in archives and feeds specified in ``:tag:``, ``:category:``, etc. options.
+
+You can also include a list of posts using :rst:dir:`postlist` directive:
+
+.. code-block:: rst
+
+ .. postlist::
+ :list-style: circle
+ :category: Manual
+ :format: {title}
+ :sort:
+
+For ABlog documentation, this converts to the following where you can find more about configuring and using ABlog:
+
+.. postlist::
+ :category: Manual
+ :list-style: circle
+ :format: {title}
+ :sort:
+
+.. only:: html
+
+ .. image:: https://readthedocs.org/projects/ablog/badge/?version=latest
+ :target: https://ablog.readthedocs.io
+
+.. toctree::
+ :hidden:
+ :glob:
+
+ */*
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..d85ed39
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.https://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+
+:end
+popd
diff --git a/docs/manual/ablog-commands.rst b/docs/manual/ablog-commands.rst
new file mode 100644
index 0000000..c50cbd7
--- /dev/null
+++ b/docs/manual/ablog-commands.rst
@@ -0,0 +1,178 @@
+.. _commands:
+
+ABlog Commands
+==============
+
+.. post:: Mar 1, 2015
+ :tags: config, commands
+ :author: Ahmet, Mehmet
+ :category: Manual
+ :location: SF
+
+
+``ablog`` commands are for streamlining blog operations, i.e. building, serving, and viewing blog pages, as well as starting a new blog::
+
+ $ ablog
+ usage: ablog [-h] [-v] {start,build,clean,serve,post,deploy} ...
+
+ ABlog for blogging with Sphinx
+
+ optional arguments:
+ -h, --help show this help message and exit
+ -v, --version print ABlog version and exit
+
+ subcommands:
+ {start,build,clean,serve,post,deploy}
+ start start a new blog project
+ build build your blog project
+ clean clean your blog build files
+ serve serve and view your project
+ post create a blank post
+ deploy deploy your website build files
+
+ See 'ablog <command> -h' for more information on a specific command.
+
+.. contents:: Here are all the things you can do:
+ :local:
+ :backlinks: top
+
+.. _start:
+
+Start a New Project
+-------------------
+
+``ablog start`` command is for quickly setting up a blog project.
+See :ref:`quick-start` for how it works and what it prepares for you::
+
+ $ ablog start -h
+ usage: ablog start [-h]
+
+ Start a new blog project by answering a few questions. You will end up with a
+ configuration file and sample pages.
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+.. _build:
+
+Build your Website
+------------------
+
+Running ``ablog build`` in your project folder builds your website HTML pages::
+
+ $ ablog build -h
+ usage: ablog build [-h] [-a] [-b BUILDER] [-s SOURCEDIR] [-w WEBSITE]
+ [-d DOCTREES] [-T] [-P]
+
+ Path options can be set in conf.py. Default values of paths are relative to
+ conf.py.
+
+ optional arguments:
+ -h, --help show this help message and exit
+ -a write all files; default is to only write new and changed
+ files
+ -b BUILDER builder to use, default `ablog_builder` or dirhtml
+ -s SOURCEDIR root path for source files, default is path to the folder that
+ contains conf.py
+ -w WEBSITE path for website, default is _website when `ablog_website` is
+ not set in conf.py
+ -d DOCTREES path for the cached environment and doctree files, default
+ .doctrees when `ablog_doctrees` is not set in conf.py
+ -T show full traceback on exception
+ -P run pdb on exception
+
+Serve and View Locally
+----------------------
+
+Running ``ablog serve``, after building your website, will start a Python server and open up browser tab to view your website::
+
+ $ ablog serve -h
+ usage: ablog serve [-h] [-w WEBSITE] [-p PORT] [-n] [-r] [--patterns]
+
+ Serve options can be set in conf.py. Default values of paths are relative to
+ conf.py.
+
+ optional arguments:
+ -h, --help show this help message and exit
+ -w WEBSITE path for website, default is _website when `ablog_website` is
+ not set in conf.py
+ -p PORT port number for HTTP server; default is 8000
+ -n do not open website in a new browser tab
+ -r rebuild when a file matching patterns change or get added
+ --patterns patterns for triggering rebuilds
+
+.. _deploy:
+
+Deploy to GitHub Pages
+----------------------
+
+Running ``ablog deploy`` will push your website to GitHub::
+
+ $ ablog deploy -h
+ usage: ablog deploy [-h] [-w WEBSITE] [-p REPODIR] [-g GITHUB_PAGES]
+ [-m MESSAGE] [-f] [--push-quietly]
+ [--github-branch GITHUB_BRANCH] [--github-ssh]
+ [--github-token GITHUB_TOKEN] [--github-url GITHUB_URL]
+
+ Path options can be set in conf.py. Default values of paths are relative to
+ conf.py.
+
+ options:
+ -h, --help show this help message and exit
+ -w WEBSITE path for website, default is _website when
+ `ablog_website` is not set in conf.py
+ -p REPODIR path to the location of repository to be deployed, e.g.
+ `../username.github.io`, default is folder containing
+ `conf.py`
+ -g GITHUB_PAGES GitHub username for deploying to GitHub pages
+ -m MESSAGE commit message
+ -f overwrite last commit, i.e. `commit --amend; push -f`
+ --push-quietly be more quiet when pushing changes
+ --github-branch GITHUB_BRANCH
+ Branch to use. Default is 'master'.
+ --github-ssh use ssh when cloning website
+ --github-token GITHUB_TOKEN
+ environment variable name storing GitHub access token
+ --github-url GITHUB_URL
+ Custom GitHub URL. Useful when multiple accounts are
+ configured on the same machine. Default is:
+ git@github.com
+
+
+Create a Post
+-------------
+
+Finally, ``ablog post`` will make a new post template file::
+
+ $ ablog post -h
+ usage: ablog post [-h] [-t TITLE] filename
+
+ positional arguments:
+ filename filename, e.g. my-nth-post (.rst appended)
+
+ optional arguments:
+ -h, --help show this help message and exit
+ -t TITLE post title; default is formed from filename
+
+Clean Build Files
+-----------------
+
+In case you needed, running ``ablog clean`` will remove build files and do a deep clean with ``-D`` option::
+
+ $ ablog clean -h
+ usage: ablog clean [-h] [-d DOCTREES] [-w WEBSITE] [-D]
+
+ Path options can be set in conf.py. Default values of paths are relative to
+ conf.py.
+
+ optional arguments:
+ -h, --help show this help message and exit
+ -d DOCTREES path for the cached environment and doctree files, default
+ .doctrees when `ablog_doctrees` is not set in conf.py
+ -w WEBSITE path for website, default is _website when `ablog_website` is
+ not set in conf.py
+ -D deep clean, remove cached environment and doctree files
+
+.. update:: Apr 7, 2015
+
+ Added ``ablog clean`` and ``ablog deploy`` commands.
diff --git a/docs/manual/ablog-configuration-options.rst b/docs/manual/ablog-configuration-options.rst
new file mode 100644
index 0000000..9b25fb3
--- /dev/null
+++ b/docs/manual/ablog-configuration-options.rst
@@ -0,0 +1,291 @@
+.. _config:
+
+ABlog Configuration Options
+===========================
+
+.. post:: May 10, 2014
+ :tags: config
+ :author: Ahmet
+ :category: Manual
+ :location: Pittsburgh
+
+
+This post describes ABlog configuration options that go in :ref:`Sphinx build configuration file <sphinx:build-config>`.
+
+General options
+---------------
+
+.. confval:: blog_path
+
+ A path relative to the configuration directory for blog archive pages.
+ Default is ``'blog'``.
+
+Authors, languages, & locations
+-------------------------------
+
+.. confval:: blog_authors
+
+ A dictionary of author names mapping to author full display names and links.
+ Dictionary keys are what should be used in ``post`` directive to refer to the author.
+ Default is ``{}``.
+ Example::
+
+ blog_authors = {
+ 'Ahmet': ('Ahmet Bakan', 'http://ahmetbakan.com'),
+ 'Durden': ('Tyler Durden',
+ 'https://en.wikipedia.org/wiki/Tyler_Durden'),
+ }
+
+.. confval:: blog_default_author
+
+ Name of the default author defined in :confval:`blog_authors`.
+ Default is ``None``.
+
+.. confval:: blog_languages
+
+ A dictionary of language code names mapping to full display names and links of these languages.
+ Similar to :confval:`blog_authors`, dictionary keys should be used in ``post`` directive to refer to the locations.
+ Default is ``{}``.
+ Example::
+
+ blog_languages = {
+ 'en': ('English', None),
+ }
+
+.. confval:: blog_default_language
+
+ Code name of the default language defined in :confval:`blog_languages`.
+ Default is ``None``.
+
+.. confval:: blog_locations
+
+ A dictionary of location names mapping to full display names and links of these locations.
+ Similar to :confval:`blog_authors`, dictionary keys should be used in ``post`` directive to refer to the locations.
+ Default is ``{}``.
+
+.. confval:: blog_default_location
+
+ Name of the default location defined in :confval:`blog_locations`.
+ Default is ``None``.
+
+.. update:: Sep 15, 2014
+
+ Added :confval:`blog_languages` and :confval:`blog_default_language` configuration variables.
+
+Post related
+------------
+
+.. confval:: post_date_format
+
+ Date display format (default is ``'%b %d, %Y'``, e.g., ``12 August 2024``) for published posts that goes as input to :meth:`datetime.date.strftime`.
+
+.. confval:: post_date_format_short
+
+ Date display format in recent posts sidebar (default is ``'%d %B'``, e.g., ``12 October``) for published posts that goes as input to :meth:`datetime.date.strftime`.
+
+.. confval:: post_auto_excerpt
+
+ Number of paragraphs (default is ``1``) that will be displayed as an excerpt from the post.
+ Setting this ``0`` will result in displaying no post excerpt in archive pages.
+ This option can be set on a per post basis using :rst:dir:`post` directive option ``excerpt``.
+
+ See :ref:`post-excerpts-and-images` for a more detailed discussion.
+
+.. confval:: post_auto_image
+
+ Index of the image that will be displayed in the excerpt of the post.
+ Default is ``0``, meaning no image.
+ Setting this to ``1`` will include the first image, when available, to the excerpt.
+ This option can be set on a per post basis using :rst:dir:`post` directive option ``image``.
+
+.. confval:: post_redirect_refresh
+
+ Number of seconds (default is ``5``) that a redirect page waits before refreshing the page to redirect to the post.
+
+.. confval:: post_always_section
+
+ When ``True``, post title and excerpt is always taken from the section that contains the :rst:dir:`post` directive, instead of the document.
+ This is the behavior when :rst:dir:`post` is used multiple times in a document.
+ Default is ``False``.
+
+.. confval:: post_show_prev_next
+
+ When ``True``, links to the previous and next posts will be rendered at the bottom of the page.
+ Default is ``True``
+
+Blog feeds
+----------
+
+Turn feeds on by setting :confval:`blog_baseurl` configuration variable.
+
+.. confval:: blog_baseurl
+
+ Base URL for the website, turns on generating feeds. E.g., ``https://ablog.readthedocs.io``.
+
+Then optionally set the following:
+
+.. confval:: blog_title
+
+ The “title” for the blog, used in feeds title (not archive web pages title). Default is ``'Blog'``.
+
+.. confval:: blog_archive_titles
+
+ Choose to archive only post titles in collection pages, default is ``False``.
+
+.. confval:: blog_feed_archives
+
+ Choose to create feeds per author, location, tag, category, and year, default is ``False``.
+
+.. confval:: blog_feed_fulltext
+
+ Choose to display full text in blog feeds, default is ``False``.
+
+.. confval:: blog_feed_subtitle
+
+ Blog feed subtitle, default is ``None``.
+
+.. confval:: blog_feed_titles
+
+ Choose to feed only post titles, default is ``False``.
+
+.. confval:: blog_feed_templates
+
+ A dictionary of feed filename roots mapping to nested dictionaries of feed entry
+ elements, ``title``, ``summary``, and/or ``content``, and a `Jinja2`_ template which will be
+ used to render the value used for that element in that feed. Templates are rendered
+ with the the following context:
+ - ``feed_length``
+ - ``feed_fulltext``
+ - ``feed_posts``
+ - ``pagename``
+ - ``feed_title``
+ - ``feed_url``
+ - ``feed``
+ - ``post``
+ - ``post_url``
+ - ``content``
+ - ``feed_entry``
+ - ``title``
+ - ``summary``
+ - ``blog``
+ - ``url``
+ - ``app``
+ Default is: ``{"atom": {}}``
+ Example to add an additional feed for posting to social media::
+
+ blog_feed_templates = {
+ # Use defaults, no templates
+ "atom": {},
+ # Create content text suitable posting to social media
+ "social": {
+ # Format tags as hashtags and append to the content
+ "content": "{{ title }}{% for tag in post.tags %}"
+ " #{{ tag.name|trim()|replace(' ', '') }}"
+ "{% endfor %}",
+ },
+ }
+
+.. confval:: blog_feed_length
+
+ Specify number of recent posts to include in feeds, default is ``None`` for all posts.
+
+.. update:: Aug 24, 2014
+
+ Added :confval:`blog_feed_archives`, :confval:`blog_feed_fulltext`, :confval:`blog_feed_subtitle`, and :confval:`post_always_section` options.
+
+.. update:: Nov 27, 2014
+
+ Added :confval:`blog_feed_titles`, :confval:`blog_feed_length`, and :confval:`blog_archive_titles` options.
+
+.. update:: Mar 20, 2021
+
+ Added :confval:`blog_feed_templates` option.
+
+.. _fa:
+.. _Jinja2: https://jinja.palletsprojects.com/
+
+.. _font-awesome:
+
+Font awesome
+------------
+
+ABlog templates will use of `Font Awesome`_ icons if one of the following is set:
+
+.. _Font Awesome: https://fontawesome.com/
+
+.. confval:: fontawesome_link_cdn
+
+ URL to `Font Awesome`_ :file:`.css` hosted at `Bootstrap CDN`_ or anywhere else.
+ Default: ``None``
+
+ .. _Bootstrap CDN: https://www.bootstrapcdn.com/fontawesome/
+
+.. update:: Jul 29, 2015
+
+ :confval:`fontawesome_link_cdn` was a *boolean* option, and now became a *string* to enable using desired version of `Font Awesome`_.
+ To get the old behavior, use ``‘https://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css'``.
+
+.. confval:: fontawesome_included
+
+ Sphinx_ theme already links to `Font Awesome`_.
+ Default: ``False``
+
+Alternatively, you can provide the path to `Font Awesome`_ :file:`.css` with the following configuration option:
+
+.. confval:: fontawesome_css_file
+
+ Path to `Font Awesome`_ :file:`.css` (default is ``None``) that will be linked to in HTML output by ABlog.
+
+.. _disqus-integration:
+
+Disqus integration
+------------------
+
+Of course one cannot think of a blog that doesn't allow for visitors to comment.
+You can enable Disqus_ by setting :confval:`disqus_shortname` and :confval:`blog_baseurl` variables.
+The reason for requiring :confval:`blog_baseurl` to be specified as of v0.7.2 is to ensure that Disqus associates correct URLs with threads when you serve new posts locally for the first time.
+
+.. confval:: disqus_shortname
+
+ Disqus_ short name for the website.
+
+.. confval:: disqus_pages
+
+ Choose to disqus pages that are not posts, default is ``False``.
+
+.. confval:: disqus_drafts
+
+ Choose to disqus posts that are drafts (without a published date), default is ``False``.
+
+Isso integration
+----------------
+
+An alternative to Disqus, is `Isso <https://isso-comments.de/>`__.
+Integration is provided by `sphinxnotes-isso`_ and the instructions there.
+
+.. _sphinxnotes-isso: https://sphinx-notes.github.io/isso/
+
+Command Options
+---------------
+
+.. update:: Apr 7, 2015
+
+ Added :ref:`commands` options.
+
+.. confval:: ablog_website
+
+ Directory name for build output files. Default is ``_website``.
+
+.. confval:: ablog_doctrees
+
+ Directory name for build cache files. Default is ``.doctrees``.
+
+.. confval:: ablog_builder
+
+ HTML builder, default is ``dirhtml``. Build HTML pages, but with a single directory per document.
+ Makes for prettier URLs (no .html) if served from a webserver. Alternative is ``html`` to build one HTML file per document.
+
+.. confval:: github_pages
+
+ GitHub user name used by ``ablog deploy`` command.
+ See :ref:`deploy` and :ref:`deploy-to-github-pages` for more information.
diff --git a/docs/manual/ablog-i18n.rst b/docs/manual/ablog-i18n.rst
new file mode 100644
index 0000000..602a044
--- /dev/null
+++ b/docs/manual/ablog-i18n.rst
@@ -0,0 +1,56 @@
+ABlog Internationalization
+==========================
+
+.. post:: Aug 30, 2014
+ :tags: i18n
+ :category: Manual
+ :author: Luc, Ahmet
+ :language: Chinese
+
+ABlog automatically generates certain labels like :ref:`blog-posts` and :ref:`blog-categories`.
+If these labels appear in English on your blog although you specified another language, then this page is for you.
+
+ABlog needs your help for translation of these labels.
+Translation process involves the following steps:
+
+* Update translatable messages:
+
+ Execute extract_messages_ each time a translatable message text is changed or added::
+
+ $ python setup.py extract_messages -o ablog/locales/sphinx.pot
+ ...
+
+ This will create or update :file:`ablog/locales/sphinx.pot` file, the central messages catalog used by the different translations.
+
+Either:
+
+* Create new translation catalog:
+
+ Execute init_catalog_ once for each *new* language, e.g.::
+
+ $ python setup.py init_catalog -l de -i ablog/locales/sphinx.pot -o ablog/locales/de/LC_MESSAGES/sphinx.po
+
+ This will create a file :file:`ablog/locales/de/LC_MESSAGES/sphinx.po` in which translations needs to be placed.
+
+* Update translation catalog:
+
+ Execute update_catalog_ for each *existing* language, e.g.::
+
+ $ python setup.py update_catalog -l de -i ablog/locales/sphinx.pot -o ablog/locales/de/LC_MESSAGES/sphinx.po
+
+ This will update file :file:`ablog/locales/de/LC_MESSAGES/sphinx.po` where translations of new text needs to be placed.
+
+Finally:
+
+* Compile catalogs:
+
+ Execute compile_catalog_ for each existing language, e.g::
+
+ $ python setup.py compile_catalog --directory ablog/locales/ --domain sphinx --locale de
+
+ If you remove ``--locale de`` then all catalogs will be compiled.
+
+.. _extract_messages: https://babel.pocoo.org/en/latest/setup.html#extract-messages
+.. _init_catalog: https://babel.pocoo.org/en/latest/setup.html#init-catalog
+.. _update_catalog: https://babel.pocoo.org/en/latest/setup.html#update-catalog
+.. _compile_catalog: https://babel.pocoo.org/en/latest/setup.html#compile-catalog
diff --git a/docs/manual/ablog-quick-start.rst b/docs/manual/ablog-quick-start.rst
new file mode 100644
index 0000000..128bb59
--- /dev/null
+++ b/docs/manual/ablog-quick-start.rst
@@ -0,0 +1,126 @@
+.. _quick-start:
+
+
+ABlog Quick Start
+=================
+
+.. post:: Mar 1, 2015
+ :tags: config, tips
+ :author: Mehmet, Ahmet
+ :category: Manual
+ :location: SF
+
+This short walk through of blogging work flow assumes that you have already installed ABlog. If not, see :ref:`installation` guide.
+
+*Note that this post is a working draft. Feel free to revise it on GitHub.*
+
+Start a Project
+---------------
+
+To start a new project, run ``ablog start`` command in a directory where you want to keep your project source files.
+This command will ask you a few questions and create the following files:
+
+ * :file:`conf.py` that contains project configuration for building HTML pages.
+
+ * :file:`first-post.rst`, a blog post example.
+
+ * :file:`index.rst` that contains content for the *landing* page of your website.
+
+ * :file:`about.rst`, another non-post page example.
+
+
+Build and View
+--------------
+
+With no further delay, let's see what your project will look like.
+First run ``ablog build``, in your project folder, to have HTML pages built in :file:`_website` folder.
+Then, call ``ablog serve`` to view them in your default web browser.
+See :ref:`commands` for more information about these commands.
+
+Your landing page is built from :file:`index.rst` and contains links to your first post and about page.
+Take a look at :file:`index.rst` for some tips on navigation links within the project.
+
+Write Content
+-------------
+
+If you are new to Sphinx_ and reStructuredText markup language, you might find `reStructuredText Primer <https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`__ useful.
+
+Pages
+^^^^^
+
+Pages in your project are :file:`.rst` files that are only a :rst:dir:`post` directive short of becoming blog posts.
+To make regular pages accessible from the navigation bar, you need to list them in a :rst:dir:`toctree`.
+This is shown for *about* page into :file:`index.rst`.
+
+Posts
+^^^^^
+
+You can convert any page to a post with a :rst:dir:`post` directive.
+ABlog will take care of listing posts in specified archives and sidebars.
+
+Blog posts
+^^^^^^^^^^
+
+You can start new blog posts with either a front-matter or a directive using ABlog.
+Simply use something based on the following template as the front-matter::
+
+:blogpost: true
+:date: January 1, 2020
+:author: A. Author
+:location: World
+:category: Blog
+:language: English
+:tags: blog
+
+Simply use something based on the following template as the directive for ABlog::
+
+ .. post:: January 1, 2020
+
+ :author: A. Author
+ :location: World
+ :category: Blog
+ :language: English
+ :tags: blog
+
+For more information, see :ref:`posting-directive` and :ref:`posting-front-matter`.
+
+Comments
+--------
+
+You can enable comments in your website by creating a Disqus_ account and obtaining a unique identifier, i.e. :confval:`disqus_shortname`.
+See :ref:`disqus-integration` for configuration options.
+
+Analytics
+---------
+
+ABlog uses Alabaster_ theme by default. You can use theme options to set your `Google Analytics`__ identifier to enable tracking.
+
+__ https://www.google.com/analytics/
+
+Configuration
+-------------
+
+There are four major groups of configuration options that can help you customize how your website looks:
+
+ * :ref:`config` - add blog authors, post locations and languages to your blog, adjust archive and feed content, etc.
+
+ * `General configuration <https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration>`__ and `project information <https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information>`__
+
+ * :ref:`html-options` - configure appearance of your website.
+
+ * Alabaster_ theme options - link to your GitHub account and project, set up tracking, etc.
+
+Other Folders
+-------------
+
+You might have noticed that your project contains three folders that we have not mention yet.
+Here they are:
+
+ * :file:`_static` is for keeping image, :file:`.js`, and :file:`.css` files.
+ :confval:`html_static_path` Sphinx option for more information.
+
+ * :file:`_templates` is for custom HTML templates.
+ See :confval:`templates_path` for more information.
+
+ * :file:`.doctree` folder, created after build command is called, is where Sphinx_ stores the state of your project.
+ Files in this folder saves time when you rebuild your project.
diff --git a/docs/manual/api.rst b/docs/manual/api.rst
new file mode 100644
index 0000000..9704c3c
--- /dev/null
+++ b/docs/manual/api.rst
@@ -0,0 +1,20 @@
+.. api:
+
+ABlog API
+=========
+
+.. post:: Feb 17, 2018
+ :tags: api
+ :author: Nabil Freij
+ :category: Manual
+ :location: World
+
+.. automodapi:: ablog
+
+.. automodapi:: ablog.blog
+
+.. automodapi:: ablog.commands
+
+.. automodapi:: ablog.post
+
+.. automodapi:: ablog.start
diff --git a/docs/manual/cross-referencing-blog-pages.rst b/docs/manual/cross-referencing-blog-pages.rst
new file mode 100644
index 0000000..bd4b86b
--- /dev/null
+++ b/docs/manual/cross-referencing-blog-pages.rst
@@ -0,0 +1,47 @@
+Cross-referencing Blog Pages
+============================
+
+.. post:: May 11, 2014
+ :tags: tips, Sphinx
+ :category: Manual
+ :location: Pittsburgh
+ :author: Ahmet
+
+ABlog creates references to all post and archive pages.
+Posts can be cross-referenced using the name of the file, or when the file is named :file:`index`, the name of the folder that contains the file.
+
+This page, :ref:`cross-referencing-blog-pages`, for example is referenced as ``:ref:`cross-referencing-blog-pages``` using :rst:role:`ref` role.
+
+When posts have long file names, it may be inconvenient to use them repeatedly for cross-referencing.
+An alternative that Sphinx_ offers is creating your own short and unique labels for cross-referencing to posts. See :ref:`xref-syntax` for details.
+
+.. _archives:
+
+Archive pages
+-------------
+
+Archive pages, on the other hand, can be cross-referenced by combining archive type and archive name as follows:
+
+============== ========================== ===============================
+Archive Example reStructured Text
+============== ========================== ===============================
+Posts :ref:`blog-posts` ``:ref:`blog-posts```
+Drafts :ref:`blog-drafts` ``:ref:`blog-drafts```
+Blog Feed :ref:`blog-feed` ``:ref:`blog-feed```
+Author :ref:`author-ahmet` ``:ref:`author-ahmet```
+Language :ref:`language-en` ``:ref:`language-en```
+Location :ref:`location-pittsburgh` ``:ref:`location-pittsburgh```
+============== ========================== ===============================
+
+Following archive pages list all posts by grouping them:
+
+============== ========================== ===============================
+Archive Example reStructured Text
+============== ========================== ===============================
+By tag :ref:`blog-tags` ``:ref:`blog-tags```
+By author :ref:`blog-authors` ``:ref:`blog-authors```
+By language :ref:`blog-languages` ``:ref:`blog-languages```
+By location :ref:`blog-locations` ``:ref:`blog-locations```
+By category :ref:`blog-categories` ``:ref:`blog-categories```
+By archive :ref:`blog-archives` ``:ref:`blog-archives```
+============== ========================== ===============================
diff --git a/docs/manual/deploy-to-github-pages.rst b/docs/manual/deploy-to-github-pages.rst
new file mode 100644
index 0000000..c98da9a
--- /dev/null
+++ b/docs/manual/deploy-to-github-pages.rst
@@ -0,0 +1,41 @@
+
+Deploy to GitHub Pages
+======================
+
+
+.. post:: Apr 07, 2015
+ :tags: deploy
+ :author: Ahmet
+ :category: Manual
+ :location: SF
+
+If you are looking for a place to publish your blog, `GitHub Pages`__ might be the place for you.
+
+__ https://pages.github.com/
+
+Assuming that you have a GitHub account, here are what you need to do to get published:
+
+1. Head over to GitHub_ and create a new repository named ``username.github.io``, where username is your username (or organization name) on GitHub.
+
+2. (optional) If you followed the link, you might as well give a star to ABlog ;)
+
+3. Set :confval:`github_pages` configuration variable in :file:`conf.py` file.
+
+4. Run ``ablog build`` in your project folder.
+
+5. Run ``ablog deploy``. This command will
+
+ i. clone your GitHub pages repository to project folder,
+
+ ii. copy all files from build folder (:file:`_website`) to :file:`username.github.io`,
+
+ iii. add and commit copied files,
+
+ iv. add `.nojekyll <https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site#troubleshooting-publishing-from-a-branch>`_
+ file, since this ain't no Jekyll_
+
+ v. and finally push the changes to publish.
+
+Let us know how this works for you!
+
+.. _Jekyll: https://jekyllrb.com/
diff --git a/docs/manual/external-post.rst b/docs/manual/external-post.rst
new file mode 100644
index 0000000..9eb19ab
--- /dev/null
+++ b/docs/manual/external-post.rst
@@ -0,0 +1,10 @@
+:blogpost: true
+:date: September 01, 2021
+:author: Chris
+:category: Manual
+:external_link: https://www.sphinx-doc.org/en/master/
+
+External post
+=============
+
+This text will be in auto-generated post previews, but links to the post will direct to ``external_link``.
diff --git a/docs/manual/forever-draft.rst b/docs/manual/forever-draft.rst
new file mode 100644
index 0000000..7ef72d7
--- /dev/null
+++ b/docs/manual/forever-draft.rst
@@ -0,0 +1,33 @@
+Draft Example
+=============
+
+.. post::
+ :tags: draft
+ :category: Manual
+
+
+As the title suggests, this is a draft example and shall remain so until the end of time or internet.
+
+How do you draft a post?
+------------------------
+
+Just indicate that the page is a post using :rst:dir:`post` directive, but do not provide give a published date:
+
+.. code-block:: rst
+
+ .. post::
+ :tags: draft
+ :category: Manual
+
+You can still label a post you are drafting with tags and categories, but the post will not be listed in corresponding archive pages until it is published.
+
+How can you see a list of drafts?
+---------------------------------
+
+See :ref:`blog-drafts` archive page, which can be referred to as ``:ref:`blog-drafts```.
+
+Why would you make a post draft?
+--------------------------------
+
+Let's say you are using Disqus_ on your website, and allowing non-post pages to be discussed as well, but you don't want a draft to be discussed before it is published.
+By adding :rst:dir:`post` directive without published date and keeping configuration variable :confval:`disqus_drafts` as ``False``, you can achieve that.
diff --git a/docs/manual/images/notebook_cells.png b/docs/manual/images/notebook_cells.png
new file mode 100644
index 0000000..1411a86
--- /dev/null
+++ b/docs/manual/images/notebook_cells.png
Binary files differ
diff --git a/docs/manual/markdown.md b/docs/manual/markdown.md
new file mode 100644
index 0000000..63a38aa
--- /dev/null
+++ b/docs/manual/markdown.md
@@ -0,0 +1,56 @@
+---
+blogpost: true
+date: Oct 10, 2020
+author: Nabil Freij
+location: World
+category: Manual
+language: English
+---
+
+# Markdown Support
+
+ABlog can support markdown pages using [myst-parser](https://pypi.org/project/myst-parser/).
+This page is a markdown file underneath.
+
+You will need to do a few things to get setup.
+
+1. Install [myst-parser](https://pypi.org/project/myst-parser/)
+2. Add these options to your config, `conf.py`
+
+```python
+extensions = [
+ ...
+ "myst_parser",
+ ...
+]
+myst_update_mathjax = False
+```
+
+Then use the new blogpost metadata format (with a slight twist):
+
+```
+---
+blogpost: true
+date: Oct 10, 2020
+author: Nabil Freij
+location: World
+category: Manual
+language: English
+---
+```
+
+Notice here we do not have a ":" at the start since the markdown metadata format is different from rst.
+
+Please be aware that adding "myst-parser" will mean it will read all markdown files and try to parse them.
+You will need to use the following in your `conf.py` to prevent this:
+
+```python
+exclude_patterns = [
+ "posts/*/.ipynb_checkpoints/*",
+ ".github/*",
+ ".history",
+ "github_submodule/*",
+ "LICENSE.md",
+ "README.md",
+]
+```
diff --git a/docs/manual/notebook_support.ipynb b/docs/manual/notebook_support.ipynb
new file mode 100644
index 0000000..0a5b023
--- /dev/null
+++ b/docs/manual/notebook_support.ipynb
@@ -0,0 +1,81 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Jupyter Notebook Posting"
+ ]
+ },
+ {
+ "cell_type": "raw",
+ "metadata": {
+ "raw_mimetype": "text/restructuredtext"
+ },
+ "source": [
+ ".. post:: 27 Oct 2018\n",
+ " :author: Nabil Freij, Second Author\n",
+ " :tags: posting\n",
+ " :category: Manual"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To add support for Notebooks to your Ablog instance, you need to configure your `docs/conf.py` (or wherever your `conf.py` is located.\n",
+ "\n",
+ "You will need to add\n",
+ "\n",
+ "```\n",
+ " extensions = [..., 'nbsphinx', ...]\n",
+ " exclude_patterns = ['docs/manual/.ipynb_checkpoints/*'] (To exclude the notebook autosaves)\n",
+ "```\n",
+ "\n",
+ "You will need to install [nbsphinx](https://nbsphinx.readthedocs.io/) either from `Anaconda` or `pip`. You might need to install [ipython](https://ipython.org/) to make sure the notebook can be run."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Within the notebook you need to make sure the cells are in this order: Titlte cell, post cell. So for this notebook, it looks like this: ![posting](images/notebook_cells.png)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "So the information is similar to how you create a normal RST post."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Another working example is SunPy's website which runs [Ablog](https://sunpy.org/blog.html). The Pull Request that added support can be found [here](https://github.com/sunpy/sunpy.org/pull/131) and how to link them to a [Binder](https://mybinder.org/) instance [here](https://github.com/sunpy/sunpy.org/pull/134)."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/docs/manual/post-excerpts-and-images.rst b/docs/manual/post-excerpts-and-images.rst
new file mode 100644
index 0000000..68cd106
--- /dev/null
+++ b/docs/manual/post-excerpts-and-images.rst
@@ -0,0 +1,44 @@
+Post Excerpts and Images
+========================
+
+.. post:: May 12, 2014
+ :tags: directive
+ :category: Manual
+ :location: Pittsburgh
+ :author: Ahmet
+ :exclude:
+ :image: 2
+
+ This post describes how to choose an excerpt and an image image for a post to be displayed in archive pages.
+
+Excerpts
+--------
+
+ABlog, by default, uses first paragraph of the document as post excerpt.
+Default number of paragraphs to use in excerpts is controlled via :confval:`post_auto_excerpt` configuration variable.
+This option can be overwritten using ``:excerpt:`` option in :rst:dir:`post` directive.
+
+Alternatively, you can provide some content in a post directive as follows::
+
+ .. post:: Apr 15, 2014
+
+ This is all of the excerpt for this post.
+
+This content is going to be used as excerpt in archive pages.
+Furthermore, if you do not want the excerpt to be included in the post, you can use ``:exclude:`` option as follows::
+
+ .. post:: Apr 15, 2014
+ :exclude:
+
+ This is all of the excerpt for this post.
+ It will be displayed in archive pages and excluded from the post page.
+
+Images
+------
+
+Let's first include a local and a non-local image in this post.
+
+.. image:: /_static/ablog.png
+.. image:: https://www.python.org/static/community_logos/python-logo.png
+
+To link the second one of these, we add ``:image: 2`` option in :rst:dir:`post` directive.
diff --git a/docs/manual/posting-and-listing.rst b/docs/manual/posting-and-listing.rst
new file mode 100644
index 0000000..1ae9a0f
--- /dev/null
+++ b/docs/manual/posting-and-listing.rst
@@ -0,0 +1,212 @@
+Posting and Listing
+===================
+
+.. post:: May 9, 2014
+ :tags: directive
+ :category: Manual
+ :location: Pittsburgh
+ :author: Ahmet
+
+This post describes :rst:dir:`post`, :rst:dir:`update`, and :rst:dir:`postlist` directives.
+
+.. _posting-directive:
+
+Posting with a Directive
+------------------------
+
+Any page in a Sphinx_ project can be converted to a post using the following directive:
+
+.. rst:directive:: post
+
+ All possible directive options are shown below::
+
+ .. post:: 15 Apr, 2013
+ :tags: tips, ablog, directive
+ :category: Example, How To
+ :author: Ahmet, Durden
+ :location: Pittsburgh, SF
+ :redirect: blog/old-page-name-for-the-post
+ :excerpt: 2
+ :image: 1
+ :external_link: https://anexternalwebsite.org
+ :nocomments:
+
+ **Drafts & Posts**
+
+ Posts without dates or with future dates are considered as drafts and are published only in :ref:`blog-drafts` archive page.
+
+ Posts with dates that are older than the day Sphinx project is built are published in :ref:`blog-posts` page.
+
+ Post date format must follow the format specified with confval:`post_date_format` configuration option.
+
+ **Tags & Categories**
+
+ You can specify multiple tags and categories by separating them with commas.
+ Posts will be listed in archive pages generated for each unique tag and category.
+
+ **Authors, Languages, & Locations**
+
+ Likewise, you can specify authors, languages, and locations of a post using ``:author:``, ``:language:``, and ``:location:`` options.
+ All of these option names are in their singular form, but multiple values separated by commas are accepted.
+
+ Using :confval:`blog_authors`, :confval:`blog_languages`, and :confval:`blog_locations` configuration variables, you can also provide home pages and/or full display names of authors, languages, and locations, which will be displayed in archive pages generated for all unique authors, languages, and locations.
+
+ **Redirections**
+
+ You can make ABlog create pages that will redirect to current post using ``:redirect:`` option. It takes a comma separated list of paths, relative to the root folder.
+ The redirect page waits for :confval:`post_redirect_refresh` seconds before redirection occurs.
+
+ **Disable comments**
+
+ You can disable comments for the current post using the ``:nocomments:`` option.
+ Currently there is no way to disable comments in a specific page.
+
+ **Excerpts & Images**
+
+ By default, ABlog uses the first paragraph of a page as post excerpt.
+ You can change this behavior and also add an image to the excerpt.
+ To find out how, see :ref:`post-excerpts-and-images`.
+
+ **Canonical links**
+
+ If you re-publish content already existing on another URL (e.g., if you re-publish content from an employer's blog your personal one), use the ``canonical_link`` parameter to create a [canonical link relation](https://datatracker.ietf.org/doc/html/rfc6596) to the original version.
+
+ **External links**
+
+ If you'd like a post to point to an external website (e.g., if you host your posts on a blogging platform like Medium but wish to maintain a list of posts on your ``Ablog`` site), use the ``external_link`` parameter and this will be used instead.
+
+ **Update Notes**
+
+ .. rst:directive:: update
+
+ Update in a post can be noted anywhere in the post as follows::
+
+ .. update:: 20 Apr, 2014
+
+ Added :rst:dir:`update` directive and :ref:`posting-sections` section.
+ Also revised the text here and there.
+
+ Update date format must follow the format specified with :confval:`post_date_format` configuration option.
+
+ Update directive renders like the updates that are at the end of this post.
+
+.. _posting-front-matter:
+
+Posting with page front-matter
+------------------------------
+
+If you'd prefer to use `page front matter <https://www.sphinx-doc.org/en/1.7/markup/misc.html>`__ instead of using a directive, you may mark a page as a "blog post" by adding the following front-matter at the top:
+
+.. code-block:: rst
+
+ :blogpost: true
+
+``ABlog`` will treat any pages with this front-matter as a blog post.
+All fields that are available to the :ref:`posting directive <posting-directive>` can be given as page-level front-matter as well.
+
+.. admonition:: Automatically detect blog posts with a ``glob`` pattern
+ :class: tip
+
+ Instead of adding ``blogpost: true`` to each page, you may also provide a pattern (or list of patterns) in your ``conf.py`` file using the ``blog_post_pattern`` option.
+ Any filenames that match this pattern will be treated as blog posts (and page front-matter will be used to classify the blog post).
+ For example, the following configuration would match all ``rst`` files in the ``posts/`` folder:
+
+ .. code-block:: python
+
+ blog_post_pattern = "posts/*.rst"
+
+ and this configuration will match all blog posts that match either ``rst`` or ``md``:
+
+ .. code-block:: python
+
+ blog_post_pattern = ["posts/*.rst", "posts/*.md"]
+
+.. _posting-sections:
+
+Posting Sections
+----------------
+
+.. post:: Aug 20, 2014
+ :tags: directive
+ :category: Manual
+ :location: SF
+ :author: Ahmet
+
+:rst:dir:`post` directive can be used multiple times in a single page to create multiple posts of different sections of the document.
+
+When :rst:dir:`post` is used more than once, post titles and excerpts are extracted from the sections that contain the directives.
+This behavior can also be set as the default behavior using :confval:`post_always_section` configuration options.
+
+Some caveats and differences from posting a document once are:
+
+ * Next and previous links at the bottom will only regard the first post in the document.
+ * Information displayed on the sidebar will belong to the first post.
+ * References for section posts is not automatically created. Labels for cross-referencing needs to be created manually, e.g., ``.. _posting-sections``. See :ref:`xref-syntax` for details.
+
+Multiple use of :rst:dir:`post` may be suitable for major additions to a previous post. For minor changes, :rst:dir:`update` directive may be preferred.
+
+Listing
+-------
+
+A list of posts can be displayed in any page using the following directive:
+
+.. rst:directive:: postlist
+
+ Following example display all the options the directive takes::
+
+ .. postlist:: 5
+ :author: Ahmet
+ :category: Manual
+ :location: Pittsburgh
+ :language: en
+ :tags: tips
+ :date: %A, %B %d, %Y
+ :format: {title} by {author} on {date}
+ :list-style: circle
+ :excerpts:
+ :sort:
+ :expand: Read more ...
+
+ This will result in a bullet list of up to 5 posts (default is all) authored by `:ref:`author-ahmet`` in `:ref:`language-en`` when he was in `:ref:`location-pittsburgh`` and posted in `:ref:`category-manual`` with tags `:ref:`tag-tips``.
+ Posts will be in ``:sort:``\ed to appear in chronological order and listed with their ``:excerpts:``.
+ Here are those posts:
+
+ .. postlist:: 5
+ :author: Ahmet
+ :category: Manual
+ :location: Pittsburgh
+ :language: en
+ :tags: tips
+ :date: %A, %B %d, %Y
+ :format: {title} by {author} on {date}
+ :list-style: circle
+ :excerpts:
+ :sort:
+ :expand: Read more ...
+
+ When no options are given all posts will be considered and they will be ordered by recency.
+ Also, note that if the current post is one of the most recent posts, it will be omitted.
+
+.. update:: Aug 21, 2014
+
+ Added :rst:dir:`update` directive and
+ :ref:`posting-sections` section.
+ Also revised the text here and there.
+
+.. update:: Sep 15, 2014
+
+ * :rst:dir:`post` directive has ``:language:`` option.
+ * :rst:dir:`postlist` directive takes arguments to filter posts.
+
+.. update:: Mar 28, 2015
+
+ Added ``:excerpts:`` option to :rst:dir:`postlist` to list posts with their excerpts.
+
+.. update:: Apr 14, 2015
+
+ Added ``:list-style:`` option to :rst:dir:`postlist` to control bullet list style.
+ *circle*, *disc*, and *none* (default) are recognized.
+
+.. update:: May 25, 2021
+
+ Added ``:expand:`` option to :rst:dir:`postlist` to add a call to action to continue reading the post.
diff --git a/docs/manual/templates-themes.rst b/docs/manual/templates-themes.rst
new file mode 100644
index 0000000..1b0fbcd
--- /dev/null
+++ b/docs/manual/templates-themes.rst
@@ -0,0 +1,101 @@
+Templating and Themes Support
+=============================
+
+.. post:: Oct 26, 2024
+ :tags: themes
+ :category: Manual
+ :author: Libor
+
+Ablog, being a Sphinx extension, has highly customizable HTML output. The generated HTML files are based on `Sphinx templates`_. You, or Sphinx themes, can partially or completely override these templates to customize the resulting HTML.
+
+.. _Sphinx templates: https://www.sphinx-doc.org/en/master/development/html_themes/templating.html
+
+.. versionchanged:: 0.11
+ The :doc:`Ablog 0.11 </release/ablog-v0.11-released>` has changed and improved the way you can customize templates and themes. Please note that this document describes the new way of customizing templates and themes support.
+
+.. _sidebars:
+
+Blog sidebars
+-------------
+
+Sidebars are a common way to provide additional information to the reader. There are seven Ablog sidebars you can include in your HTML output using the Sphinx_ :confval:`html_sidebars` configuration option (in addition to your theme sidebars).
+
+- ``ablog/postcard.html`` provides information regarding the current post (when on a post page).
+- ``ablog/recentposts.html`` lists the most recent five posts.
+- ``ablog/tagcloud.html`` provides links to archive pages generated for each tag.
+- ``ablog/category.html``, ``ablog/authors.html``, ``ablog/languages.html``, and ``ablog/locations.html`` sidebars generate lists of links to respective archive pages with the number of matching posts (e.g., "Manual (14)", "2023 (8)", "English (22)").
+
+For example, the sidebars that you see on this website on the left are:
+
+.. code-block:: python
+
+ html_sidebars = {
+ "**": [
+ # Comes from Alabaster theme
+ "about.html",
+ "searchfield.html",
+ # Ablog sidebars
+ "ablog/postcard.html",
+ "ablog/recentposts.html",
+ "ablog/tagcloud.html",
+ "ablog/categories.html",
+ "ablog/archives.html",
+ "ablog/authors.html",
+ "ablog/languages.html",
+ "ablog/locations.html",
+ ]
+ }
+
+Styling default Ablog sidebars
+------------------------------
+
+Ablog standard sidebars are wrapped in ``<div>`` with CSS classes like :samp:`ablog-sidebar-item ablog__{<template_name>}`, making them easier to style.
+
+For example, the ``recentposts.html`` template is wrapped in ``<div class="ablog-sidebar-item ablog__recentposts">``.
+
+.. seealso::
+
+ Built-in sidebars can be found in the ``ablog/`` folder in the `Ablog source code <https://github.com/sunpy/ablog/tree/main/src/ablog/templates/ablog>`_.
+
+If styling is not enough, you can override the Ablog templates in your Sphinx project or in the Sphinx theme.
+
+Partial or complete override of Ablog templates
+-----------------------------------------------
+
+To control whether Ablog injects its own templates into the Sphinx build, you can use the following ``conf.py`` configuration option:
+
+.. confval:: skip_injecting_base_ablog_templates
+
+ If set to ``True``, Ablog will not inject its own templates into the Sphinx build. This is useful if you want to completely override Ablog templates in your Sphinx project or in the Sphinx theme. The default is ``False``.
+
+Customizing templates in the project
+------------------------------------
+
+All Ablog templates are under the ``ablog/`` folder space. For example, ``ablog/postcard.html``. You can override these templates by placing them in the ``ablog/`` folder in your project templates folder.
+
+#. Add the :confval:`templates_path` option in your ``conf.py`` file:
+
+ .. code-block:: python
+
+ templates_path = ["_templates"]
+
+#. Create a folder ``_templates/`` next to your ``conf.py`` file. It will hold your custom templates.
+#. Create a folder ``ablog/`` inside the ``_templates/`` folder.
+#. Create a file here with the same name as the template you want to override. For example, ``postcard.html``. This file will be used as a custom template for the sidebar. You can copy the content of the original template from the Ablog source code and modify it as you need.
+#. Optionally: if you want to completely override all Ablog templates, set the :confval:`skip_injecting_base_ablog_templates` option to ``True``, copy all Ablog templates here, and customize them as you need.
+
+Customizing templates in the theme
+----------------------------------
+
+If you are a Sphinx theme author, you can ship customized Ablog templates in your theme. You can override Ablog templates by placing them in the ``ablog/`` folder in your theme templates, e.g., ``ablog/postcard.html``.
+
+#. In the theme root (where the ``theme.toml`` (or ``theme.ini`` in older Sphinx themes) file is), create a folder ``ablog/``.
+#. Create a file here with the same name as the template you want to override. For example, ``postcard.html``.
+#. This file will be used as a custom template for the sidebar. You can copy the content of the original template from the Ablog source code and modify it as you need.
+#. In your ``theme.toml`` file, add the following (under the ``[options]`` section):
+
+ .. code-block:: toml
+
+ ablog_inject_templates_after_theme = true
+
+ This will ensure that Ablog templates are injected *after* the theme templates, so you can override them while still using the Ablog templates as a fallback.
diff --git a/docs/manual/watch-yourself-blogging.rst b/docs/manual/watch-yourself-blogging.rst
new file mode 100644
index 0000000..84da97b
--- /dev/null
+++ b/docs/manual/watch-yourself-blogging.rst
@@ -0,0 +1,21 @@
+
+Watch Yourself Blogging
+=======================
+
+.. post:: Apr 19, 2015
+ :tags: commands, tips
+ :category: Manual
+ :author: Ahmet
+ :location: SF
+ :language: en
+
+Wouldn't you like your blog being rebuilt and served to you automatically as you are blogging on a sunny Sunday afternoon?
+It's now possible with the improved ``ablog serve`` command.
+
+First, you need to install Watchdog_ Python package, e.g. `pip install watchdog`.
+Then, you need to run ``ablog serve -r``.
+Regardless of the weather being sunny or the day of the week, your project will be rebuilt when you change a page or add a new one.
+This won't refresh your browser page though.
+Unless you want to hit refresh once in a while, you can easily find an auto refresher extension for you browser.
+
+.. _Watchdog: https://github.com/gorakhargosh/watchdog
diff --git a/docs/nitpick-exceptions b/docs/nitpick-exceptions
new file mode 100644
index 0000000..c1c9092
--- /dev/null
+++ b/docs/nitpick-exceptions
@@ -0,0 +1,8 @@
+py:class docutils.nodes.Node
+py:class docutils.nodes.General
+py:class docutils.nodes.Body
+py:class docutils.nodes.admonition
+py:class docutils.parsers.rst.directives.admonitions.BaseAdmonition
+py:class docutils.transforms.Transform
+py:class docutils.nodes.Element
+py:class docutils.nodes.Admonition
diff --git a/docs/release/ablog-v0.1-released.rst b/docs/release/ablog-v0.1-released.rst
new file mode 100644
index 0000000..37af594
--- /dev/null
+++ b/docs/release/ablog-v0.1-released.rst
@@ -0,0 +1,20 @@
+:blogpost: true
+:tags: tips
+:author: Ahmet
+:category: Release
+:location: SF
+:date: May 14, 2014
+
+ABlog v0.1 released
+===================
+
+ABlog v0.1 is released.
+
+This is the very first release, so there are no release notes in this post.
+
+Yes, this page is also a post and it resides in a folder different from
+most other posts in this blogumentation.
+
+The idea is to enable making any page in a Sphinx_ project a post so that
+users of a software package can subscribe to feeds and follow new releases
+as well as code examples added to the documentation.
diff --git a/docs/release/ablog-v0.10-released.rst b/docs/release/ablog-v0.10-released.rst
new file mode 100644
index 0000000..1609520
--- /dev/null
+++ b/docs/release/ablog-v0.10-released.rst
@@ -0,0 +1,282 @@
+ABlog v0.10 released
+====================
+
+.. post:: Nov 17, 2019
+ :author: Nabil Freij
+ :category: Release
+ :location: World
+
+ABlog v0.10 is released with the main focus being to support the latest version of Sphinx as well as Python 3 only support.
+
+Ablog V0.9.X will no longer be supported as Python 2 comes to an end in a few months and it is time people upgraded.
+
+Pull Requests merged in:
+
+`Overhaul of package underneath for python3 only <https://github.com/sunpy/ablog/pull/41>`__ from `nabobalis <https://github.com/nabobalis>`__.
+
+`Add validation for conf.py entries <https://github.com/sunpy/ablog/pull/38>`__ from `rayalan <https://github.com/rayalan>`__.
+
+`Deploy improve <https://github.com/sunpy/ablog/pull/42>`__ from `rayalan <https://github.com/rayalan>`__.
+
+`Get ablog ready for 0.10 <https://github.com/sunpy/ablog/pull/46>`__ from `nabobalis <https://github.com/nabobalis>`__.
+
+ABlog v0.10.1 released
+----------------------
+
+Pull Requests merged in:
+
+`Change StopIteration to return <https://github.com/sunpy/ablog/pull/48>`__ from `remyabel2 <https://github.com/remyabel2>`__.
+
+ABlog v0.10.2 released
+----------------------
+
+Pull Requests merged in:
+
+`Fix unclosed span tag <https://github.com/sunpy/ablog/pull/41>`__ from `ykrods <https://github.com/ykrods>`__.
+
+ABlog v0.10.3 released
+----------------------
+
+Pull Requests merged in:
+
+`Pin werkzeug to < 1 <https://github.com/sunpy/ablog/pull/53>`__ from `dstansby <https://github.com/dstansby>`__.
+
+`MNT: Fix Giles URL <https://github.com/sunpy/ablog/pull/50>`__ from `pllim <https://github.com/pllim>`__.
+
+ABlog v0.10.4 released
+----------------------
+
+Pull Requests merged in:
+
+`Add zh_CN locale <https://github.com/sunpy/ablog/pull/61>`__ from `daimon99 <https://github.com/daimon99>`__.
+
+`Add intersphinx to the extension list <https://github.com/sunpy/ablog/pull/60>`__ from `plaindocs <https://github.com/plaindocs>`__.
+
+`Fix "test5" <https://github.com/sunpy/ablog/pull/58>`__ and `Use "dirhtml" builder on Read The Docs <https://github.com/sunpy/ablog/pull/57>`__ from `blueyed <https://github.com/blueyed>`__.
+
+ABlog v0.10.5 released
+----------------------
+
+Pull Requests merged in:
+
+`Add custom GitHub URL support <https://github.com/sunpy/ablog/pull/63>`__ from `tg-m <https://github.com/tg-m>`__.
+
+ABlog v0.10.6 released
+----------------------
+
+Pull Requests merged in:
+
+`Add french locale <https://github.com/sunpy/ablog/pull/65>`__ from `kujiu <https://github.com/kujiu>`__.
+
+ABlog v0.10.7 released
+----------------------
+
+Pull Requests merged in:
+
+`Automatically add templates path to documentation <https://github.com/sunpy/ablog/pull/63>`__ from `choldgraf <https://github.com/choldgraf>`__.
+
+ABlog v0.10.8 released
+----------------------
+
+Removed the hard dependencies on alabaster and sphinx-automodapi.
+
+Replaced `werkzeug <https://pypi.org/project/Werkzeug/>`__ with `feedgen <https://pypi.org/project/feedgen/>`__ due to the former removing ATOM support.
+
+Version pin of nbsphinx has been removed.
+
+ABlog v0.10.9 released
+----------------------
+
+Pull Requests merged in:
+
+`frontmatter and blog post matching <https://github.com/sunpy/ablog/pull/63>`__ from `choldgraf <https://github.com/choldgraf>`__.
+
+ABlog v0.10.10 released
+-----------------------
+
+Pull Requests merged in:
+
+`Various Issues <https://github.com/sunpy/ablog/pull/77>`__.
+
+`Fix missing reference caused by ref with title <https://github.com/sunpy/ablog/pull/73>`__ from `ykrods <https://github.com/ykrods>`__.
+
+`Add instructions for starting new blog posts with front-matter <https://github.com/sunpy/ablog/pull/71>`__ from `kakirastern <https://github.com/kakirastern>`__.
+
+ABlog v0.10.11 released
+-----------------------
+
+Pull Requests merged in:
+
+`improving glob matching and documenting it <https://github.com/sunpy/ablog/pull/79>`__ from `choldgraf <https://github.com/choldgraf>`__.
+
+
+ABlog v0.10.12 released
+-----------------------
+
+Pull Requests merged in:
+
+`id of feed is now blog.blog_baseurl <https://github.com/sunpy/ablog/pull/83>`__.
+
+ABlog v0.10.13 released
+-----------------------
+
+Pull Requests merged in:
+
+`updated CI and py39 tests <https://github.com/sunpy/ablog/pull/86>`__.
+`Add test #87 <https://github.com/sunpy/ablog/pull/87>`__.
+`Some minor fixes <https://github.com/sunpy/ablog/pull/88>`__.
+`Ensure blog_post_pattern are relative to srcdir <https://github.com/sunpy/ablog/pull/89>`__.
+
+ABlog v0.10.14 released
+-----------------------
+
+Pull Requests merged in:
+
+`feat(feeds): Add missing Atom entry metadata <https://github.com/sunpy/ablog/pull/92>`__.
+`feat(feeds): Add entry element template support <https://github.com/sunpy/ablog/pull/93>`__.
+`misc update <https://github.com/sunpy/ablog/pull/94>`__.
+
+
+ABlog v0.10.15 released
+-----------------------
+
+Fixed `Index Out of Range with Atom Feeds <https://github.com/sunpy/ablog/issues/96>`__.
+
+ABlog v0.10.16 released
+-----------------------
+
+Pull Requests merged in:
+
+`fix(feeds): Feed validation, templates regression <https://github.com/sunpy/ablog/pull/97>`__.
+
+ABlog v0.10.17 released
+-----------------------
+
+Pull Requests merged in:
+
+`Correct draft URL <https://github.com/sunpy/ablog/pull/98>`__.
+
+ABlog v0.10.18 released
+-----------------------
+
+Pull Requests merged in:
+
+`Correct posts URL <https://github.com/sunpy/ablog/pull/99>`__.
+`Add isso integration <https://github.com/sunpy/ablog/pull/100>`__.
+
+ABlog v0.10.19 released
+-----------------------
+
+Pull Requests merged in:
+
+`Add expand option <https://github.com/sunpy/ablog/pull/104>`__.
+
+
+ABlog v0.10.20 released
+-----------------------
+
+Pull Requests merged in:
+
+`fix documentation typo in blog-drafts <https://github.com/sunpy/ablog/pull/105>`__.
+`Fix typo <https://github.com/sunpy/ablog/pull/109>`__.
+`Catalan translation <https://github.com/sunpy/ablog/pull/113>`__.
+`Fix ablog post <https://github.com/sunpy/ablog/pull/114>`__.
+
+ABlog v0.10.21 released
+-----------------------
+
+Pull Requests merged in:
+
+`Fix/multilang feed links <https://github.com/sunpy/ablog/pull/116>`__.
+
+BREAKING CHANGE - DROPPED PYTHON 3.6 SUPPORT
+
+ABlog v0.10.22 released
+-----------------------
+
+Pull Requests merged in:
+
+`Fix tags field for myst_parser <https://github.com/sunpy/ablog/pull/119>`__.
+
+ABlog v0.10.23 released
+-----------------------
+
+Pull Requests merged in:
+
+`optionally show previous / next links on post page <https://github.com/sunpy/ablog/pull/120>`__.
+`add classes to post elements <https://github.com/sunpy/ablog/pull/121>`__.
+
+
+ABlog v0.10.24 released
+-----------------------
+
+Breaking Changes:
+
+Minimum versions of packages increased:
+
+.. code-block:: bash
+
+ feedgen>=0.9.0
+ invoke>=1.6.0
+ python-dateutil>=2.8.0
+ sphinx>=4.0.0
+ watchdog>=2.0.0
+ myst-parser>=0.17.0
+ pytest>=6.0.0
+
+Pull Requests merged in:
+
+`Get rid of eval and fix #128 <https://github.com/sunpy/ablog/pull/131>`__.
+`CI Tweak <https://github.com/sunpy/ablog/pull/132>`__.
+
+ABlog v0.10.25 released
+-----------------------
+
+Pull Requests merged in:
+
+`Normalise path to posix as sphinx expects <https://github.com/sunpy/ablog/pull/134>`__.
+
+ABlog v0.10.26 released
+-----------------------
+
+Pull Requests merged in:
+
+`docs: Fix format of sphinx.ext.extlink for Sphinx 5.x <https://github.com/sunpy/ablog/pull/141>`__.
+`docs: Use ref link rather than hardcode link <https://github.com/sunpy/ablog/pull/140>`__.
+`Ci and Warnings Fix <https://github.com/sunpy/ablog/pull/142>`__.
+
+ABlog v0.10.27 released
+-----------------------
+
+Pull Requests merged in:
+
+`Improve conditional check for author metadata <https://github.com/sunpy/ablog/pull/146>`__.
+
+ABlog v0.10.28 released
+-----------------------
+
+Pull Requests merged in:
+
+`findall -> traverse for older versions of docutils <https://github.com/sunpy/ablog/pull/152>`__.
+
+ABlog v0.10.29 released
+-----------------------
+
+Pull Requests merged in:
+
+`Fix the error on some empty option value of post directive <https://github.com/sunpy/ablog/pull/155>`__.
+
+ABlog v0.10.30 released
+-----------------------
+
+Pull Requests merged in:
+
+`Sort Feed posts by date <https://github.com/sunpy/ablog/pull/172>`__.
+`Fix sidebars ablog translations <https://github.com/sunpy/ablog/pull/161>`__.
+
+ABlog v0.10.31 released
+-----------------------
+
+Pull Requests merged in:
+
+`Add external links for posts <https://github.com/sunpy/ablog/pull/112>`__.
diff --git a/docs/release/ablog-v0.11-released.rst b/docs/release/ablog-v0.11-released.rst
new file mode 100644
index 0000000..9936afd
--- /dev/null
+++ b/docs/release/ablog-v0.11-released.rst
@@ -0,0 +1,128 @@
+ABlog v0.11 released
+====================
+
+.. post:: March 23, 2023
+ :author: Nabil Freij
+ :category: Release
+ :location: World
+
+ABlog v0.11 is released with the main focus being to update and tweak the HTML templates allow themes to override the default templates.
+In addition, all ablog elements in the templates wrapped in ``ablog__*`` divs to allow custom CSS rules.
+
+We also adopt `NEP29 <https://numpy.org/neps/nep-0029-deprecation_policy.html>`__ and drop support for older versions of Python and package versions that are 24 months old or older at time of release.
+
+Added support for external links to be posts.
+
+There are several breaking changes:
+
+- 1. The template files are now in the ``templates/ablog`` folder.
+ Older templates are still in the old location but will raise a warning.
+ These will be removed in a future version, please do not use them anymore.
+ You will need to update any paths to them to add "ablog/" to the path.
+- 2. ``ablog`` has support for not injecting its own templates into the Sphinx build.
+ This is supported by add ``skip_injecting_base_ablog_templates = True`` to your configuration file.
+- 3. Minimum version of Python is >=3.9 and Sphinx is >=5.0.
+
+Pull Requests merged in:
+
+`Template rework <https://github.com/sunpy/ablog/pull/144>`__.
+
+`Add external links for posts <https://github.com/sunpy/ablog/pull/112>`__ from `Chris Holdgraf <https://github.com/choldgraf>`__.
+
+Unreleased
+----------
+
+Pull Requests merged in:
+
+`Fix theme support for Ablog <https://github.com/sunpy/ablog/pull/295>`__ from `Libor Jelínek <https://github.com/liborjelinek/>`__.
+
+ABlog v0.11.1 released
+----------------------
+
+Pull Requests merged in:
+
+`Update version handling to remove use of pkg_resources <https://github.com/sunpy/ablog/pull/211>`__
+
+ABlog v0.11.2 released
+----------------------
+
+Pull Requests merged in:
+
+`append posts to atom feed to keep post order from new to old <https://github.com/sunpy/ablog/pull/216>`__ from `lexming <https://github.com/lexming>`__.
+`avoid spurious warning about posts with front-matter and post directive <https://github.com/sunpy/ablog/pull/214>`__ from `lexming <https://github.com/lexming>`__.
+
+ABlog v0.11.3 released
+----------------------
+
+Pull Requests merged in:
+
+`use fully qualified URLs for images in atom feed <https://github.com/sunpy/ablog/pull/218>`__ from `lexming <https://github.com/lexming>`__.
+
+ABlog v0.11.4 released
+----------------------
+
+Pull Requests merged in:
+
+`Use paragraph instead of container for blog post excerpts <https://github.com/sunpy/ablog/pull/226>`__ from `dstansby <https://github.com/dstansby>`__.
+
+ABlog v0.11.5 released
+----------------------
+
+Pull Requests merged in:
+
+`Fix incorrect /div when using discuss <https://github.com/sunpy/ablog/pull/251>`__ from `Cadair <https://github.com/Cadair>`__.
+
+ABlog v0.11.6 released
+----------------------
+
+Pull Requests merged in:
+
+`Adds IT locale <https://github.com/sunpy/ablog/pull/253>`__ from `Stefano David <https://github.com/stefanodavid>`__.
+
+`Enables configuring a canonical_link for individual posts <https://github.com/sunpy/ablog/pull/258>`__ from `Hendrik Makait <https://github.com/hendrikmakait>`__.
+
+ABlog v0.11.7 released
+----------------------
+
+Pull Requests merged in:
+
+`Add stylesheet for tagcloud <https://github.com/sunpy/ablog/pull/268>`__ from `Shengyu Zhang <https://github.com/SilverRainZ>`__.
+
+`Create demo/ before running ablog start <https://github.com/sunpy/ablog/pull/269>`__ from `Shengyu Zhang <https://github.com/SilverRainZ>`__.
+
+
+`Add span to more items in templates <https://github.com/sunpy/ablog/pull/270>`__ from `Nabil Freij <https://github.com/nabobalis>`__.
+
+ABlog v0.11.8 released
+----------------------
+
+Added support for ``sphinx`` >=7.3.0
+
+ABlog v0.11.9 released
+----------------------
+
+`Make '_strip' function return as list not set. <https://github.com/sunpy/ablog/pull/280>`__ from `Joe Ziminski <https://github.com/JoeZiminski>`__.
+
+ABlog v0.11.10 released
+-----------------------
+
+Fixed wrong branch in the release process.
+
+ABlog v0.11.11 released
+-----------------------
+
+Mark Ablog parallel safe.
+
+Dropped support for Python 3.9, Sphinx less than 6.2.
+This mirrors the requirements for alabaster 1.0.0.
+
+ABlog v0.11.12 released
+-----------------------
+
+`Improve ablog-configuration-options.rst. <https://github.com/sunpy/ablog/pull/292>`__ from `Libor Jelínek <https://github.com/liborjelinek>`__.
+
+`Fix sidebars CSS naming. <https://github.com/sunpy/ablog/pull/298>`__ from `Libor Jelínek <https://github.com/liborjelinek>`__.
+
+`Ablog is NOT safe for parallel read. <https://github.com/sunpy/ablog/pull/299>`__ from `Libor Jelínek <https://github.com/liborjelinek>`__.
+
+`Fix theme support for ablog. <https://github.com/sunpy/ablog/pull/295>`__ from `Libor Jelínek <https://github.com/liborjelinek>`__.
diff --git a/docs/release/ablog-v0.2-released.rst b/docs/release/ablog-v0.2-released.rst
new file mode 100644
index 0000000..9caa3fa
--- /dev/null
+++ b/docs/release/ablog-v0.2-released.rst
@@ -0,0 +1,42 @@
+ABlog v0.2 released
+===================
+
+.. post:: Aug 31, 2014
+ :author: Ahmet
+ :category: Release
+ :location: SF
+
+ABlog v0.2 is released. This version comes with several new features:
+
+ * You can post a document multiple times, see :ref:`posting-sections`
+ for details.
+
+ * You can make note of updates in a post using :rst:dir:`update`
+ directive.
+
+ * Blog feeds for authors, locations, categories, tags, and years
+ can be enabled using :confval:`blog_feed_archives` configuration
+ variable.
+
+ * Blog Feeds can be made full text using :confval:`blog_feed_fulltext`
+ configuration variable.
+
+ * Recent posts side bar includes month and day of the posts.
+
+ABlog v0.2.1 released
+---------------------
+
+ABlog v0.2.1 is a bug fix release that solves duplicated content
+problem in full text atom feeds.
+
+ABlog v0.2.2 released
+---------------------
+
+ABlog v0.2.2 is a bug fix release that solves broken links problem
+in post lists (:issue:`12`).
+
+ABlog v0.2.3 released
+---------------------
+
+ABlog v0.2.3 is a bug fix release that solves broken links (:issue:`13`)
+and non-unique post IDs problems atom feeds.
diff --git a/docs/release/ablog-v0.3-released.rst b/docs/release/ablog-v0.3-released.rst
new file mode 100644
index 0000000..58680ae
--- /dev/null
+++ b/docs/release/ablog-v0.3-released.rst
@@ -0,0 +1,28 @@
+ABlog v0.3 released
+===================
+
+ABlog v0.3 is released. This version comes with the following core
+improvements:
+
+ * You can now specify language of a post with ``:language:`` option,
+ and an archive page will be created for each language.
+ See :confval:`blog_languages` and :confval:`blog_default_language`
+ if you are posting in multiple languages.
+
+ * You can list language archives on your website by adding
+ ``languages.html`` to :confval:`html_sidebars` configuration option.
+
+ * :rst:dir:`postlist` directive takes options to filter posts.
+
+ABlog v0.3.1 released
+---------------------
+
+ABlog v0.3.1 is a minor release to fix two issues in templates:
+
+ * Links to collection (archive) feeds is displayed only on collection page
+ (e.g. `:ref:`category-manual``), not on a catalog page that lists posts
+ for multiple collections (e.g. `:ref:`blog-categories``).
+
+ * Links to collection feeds is displayed only when they are generated
+ (see :confval:`blog_feed_archives`). Previously, links would be generated
+ to feeds that did not exist.
diff --git a/docs/release/ablog-v0.4-released.rst b/docs/release/ablog-v0.4-released.rst
new file mode 100644
index 0000000..605680f
--- /dev/null
+++ b/docs/release/ablog-v0.4-released.rst
@@ -0,0 +1,33 @@
+ABlog v0.4 released
+===================
+
+.. post:: Dec 20, 2014
+ :author: Ahmet
+ :category: Release
+ :location: SF
+
+ABlog v0.4 is released. This version comes with the following improvements
+and bug fixes:
+
+ * Added :confval:`blog_feed_titles`, :confval:`blog_feed_length`, and
+ :confval:`blog_archive_titles` configuration options (see :issue:`24`).
+
+ * Set the default for :confval:`blog_feed_archives` to ``False``, which
+ was set to ``True`` although documented to be otherwise.
+
+ * Fixed issues with :confval:`post_auto_excerpt` and
+ :confval:`post_auto_image` configuration options.
+
+ * Fixed :issue:`2`, relative size of tags being the minimum size when
+ all tags have the same number of posts. Now, mean size is
+ used, and max/min size can be controlled from template.
+
+ * Fixed :issue:`19`. Yearly archives are ordered by recency.
+
+ * Fixed duplicated post title in feeds, :issue:`21`.
+
+ * Fixed :issue:`22`, :rst:dir:`postlist` directive listing more than
+ specified number of posts.
+
+ * :rst:dir:`postlist` directive accepts arguments to format list items
+ (:issue:`20`).
diff --git a/docs/release/ablog-v0.5-released.rst b/docs/release/ablog-v0.5-released.rst
new file mode 100644
index 0000000..fcf1c12
--- /dev/null
+++ b/docs/release/ablog-v0.5-released.rst
@@ -0,0 +1,15 @@
+ABlog v0.5 released
+===================
+
+.. post:: Mar 25, 2015
+ :author: Ahmet, Mehmet
+ :category: Release
+ :location: SF
+
+ABlog v0.5 is released. This version comes with :ref:`ablog-commands` and
+a :ref:`quick-start` guide.
+
+ABlog v0.5.1 released
+---------------------
+
+Added ``:excerpts:`` option to :rst:dir:`postlist` directive.
diff --git a/docs/release/ablog-v0.6-released.rst b/docs/release/ablog-v0.6-released.rst
new file mode 100644
index 0000000..5fed938
--- /dev/null
+++ b/docs/release/ablog-v0.6-released.rst
@@ -0,0 +1,46 @@
+ABlog v0.6 released
+===================
+
+.. post:: Apr 8, 2015
+ :author: Ahmet
+ :category: Release
+ :location: SF
+
+ABlog v0.6 is released with new :ref:`ablog-commands`. You can use
+``ablog deploy`` to :ref:`deploy-to-github-pages`, and also ``ablog clean``
+to do spring cleaning every once in a while.
+
+ABlog v0.6.1 released
+---------------------
+
+ABlog v0.6.1 is released with improvements to ``ablog deploy`` command.
+It will add ``.nojekyll`` file when needed to deployments to GitHub pages.
+
+ABlog v0.6.2 released
+---------------------
+
+ABlog v0.6.2 is released to fix an issue with loading of Disqus comments
+(:issue:`33`) and interpreting non-ascii characters (:issue:`34`).
+
+ABlog v0.6.3 released
+---------------------
+
+ABlog v0.6.3 comes with Russian localisation and following enhancements:
+
+ * Added ``:list-style:`` option to :rst:dir:`postlist` to enable
+ controlling bullet list style.
+
+ * ``ablog post`` command de-slugifies filename to make the title
+ when it's not given.
+
+ABlog v0.6.4 released
+---------------------
+
+ABlog v0.6.4 comes with improved ``ablog serve`` command that helps you
+:ref:`watch-yourself-blogging`.
+
+ABlog v0.6.5 released
+---------------------
+
+ABlog v0.6.5 is a bug fix release to resolve :issue:`38`, an exception raised
+when using :rst:dir:`postlist` without specifying number of posts.
diff --git a/docs/release/ablog-v0.7-released.rst b/docs/release/ablog-v0.7-released.rst
new file mode 100644
index 0000000..a11fbc1
--- /dev/null
+++ b/docs/release/ablog-v0.7-released.rst
@@ -0,0 +1,92 @@
+ABlog v0.7 released
+===================
+
+.. post:: May 3, 2015
+ :author: Ahmet
+ :category: Release
+ :location: Denizli
+
+ABlog v0.7.0 is released to fix the long standing :issue:`1` related to
+pickling of Sphinx build environment on Read The Docs. Improvements
+also resolved issues with using LaTeX builder, improved cross-referencing
+for non-html builders.
+
+ABlog v0.7.1 released
+---------------------
+
+ABlog v0.7.1 is released to fix Python 3 import issues in :command:`ablog serve`
+command.
+
+ABlog v0.7.2 released
+---------------------
+
+ABlog v0.7.2 is released to prevent potential issues with Disqus thread URLs
+by requiring :confval:`disqus_shortname` and :confval:`blog_baseurl`
+to be specified together for Disqus integration.
+
+ABlog v0.7.3 released
+---------------------
+
+ABlog v0.7.3 makes use of `python-dateutil`__ for parsing post dates, so now you
+can be flexible with the format you use in posts. Thanks to `Andy Maloney`__
+for this improvement.
+
+__ https://pypi.python.org/pypi/python-dateutil
+__ https://github.com/amaloney
+
+ABlog v0.7.5 released
+---------------------
+
+ABlog v0.7.5 is released to fix Windows specific path resolving issue with
+archive pages. Thanks to Peter Mills for reporting this issue.
+
+ABlog v0.7.6 released
+---------------------
+
+ABlog v0.7.6 is released to fix path resolving issue that arose when
+``:excerpts:`` is used in :rst:dir:`postlist` directive. Once again, thanks
+to Peter Mills for reporting this issue. Other minor changes are:
+
+ * ``-P`` argument is added to :ref:`ablog build <build>` command to enable running pdb
+ on exceptions.
+
+ * ``conf.py`` file created by :ref:`ablog start <start>` updated to include
+ ``about.html`` sidebar that comes with Alabaster_ theme.
+
+ABlog v0.7.7 released
+---------------------
+
+ABlog v0.7.7 is released to fix path resolving :issue:`41` that arose when
+cross-references were used in post excerpts, and also post redirect
+issue in templates.
+
+ABlog v0.7.8 released
+---------------------
+
+ABlog v0.7.8 is released to fix a Python 2 issue that appears when creating
+collection pages that contain non-ascii characters in their names (:issue:`45`)
+and filename escaping issue when committing changes using
+:ref:`ablog deploy <deploy>` command (:pull:`44`).
+Thanks to `uralbash`_ for these contributions.
+
+.. _uralbash: https://github.com/uralbash
+
+ABlog v0.7.9 released
+---------------------
+
+ABlog v0.7.9 is released to fix Windows specific file renaming issue in
+:ref:`ablog deploy <deploy>` command (:issue:`46`). Thanks to `Velimir`_
+for the fix.
+
+.. _Velimir: https://github.com/montyvesselinov
+
+ABlog v0.7.10 released
+----------------------
+
+ABlog v0.7.10 is released to resolve Sphinx JSON/Pickle builder issues
+related to serialization.
+
+ABlog v0.7.12 released
+----------------------
+
+ABlog v0.7.12 (and also v0.7.11) maintenance release are available.
diff --git a/docs/release/ablog-v0.8-released.rst b/docs/release/ablog-v0.8-released.rst
new file mode 100644
index 0000000..cac2afb
--- /dev/null
+++ b/docs/release/ablog-v0.8-released.rst
@@ -0,0 +1,58 @@
+ABlog v0.8 released
+===================
+
+.. post:: Oct 12, 2015
+ :author: Ahmet
+ :category: Release
+ :location: SF
+
+ABlog v0.8.0 is released with additions and changes:
+
+ * Added ``-a`` argument to :ref:`ablog build <build>` command, with which
+ you can force rewriting all pages when rebuilding your project. Default is
+ writing only pages that have changed.
+
+ * Added ``-f`` argument to :ref:`ablog deploy <deploy>` command, with which
+ you can amend to latest commit to keep GitHub pages repository small.
+ Thanks to `uralbash`_ for this contribution.
+
+ * Added ``-p`` argument to :ref:`ablog deploy <deploy>` command, with which
+ you can specify the path to your GitHub pages repository, i.e.
+ ``username.github.io``.
+
+ * Changed :confval:`fontawesome_link_cdn` to be a string argument to enable
+ linking to desired version of `Font Awesome`_. Thanks to `Albert Mietus`_
+ for this contribution.
+
+ * Post lists font style is now controlled through CSS. Thanks to
+ `Albert Mietus`_ for this contribution as well.
+
+ * Fixed internal link resolution issue that affected atom feeds of
+ collections, i.e. feeds of posts under a category, tag, or author.
+
+.. _Font Awesome: https://fontawesome.com/
+.. _Albert Mietus: https://github.com/AlbertMietus
+.. _uralbash: https://github.com/uralbash
+
+ABlog v0.8.1 released
+---------------------
+
+ABlog v0.8.1 is released to fix atom feed linking in HTML header (:issue:`54`).
+
+ABlog v0.8.2 released
+---------------------
+
+ABlog v0.8.2 is released to fix date parsing (:issue:`58`) and Python 2.6
+installation (:issue:`59`) issues.
+
+ABlog v0.8.3 released
+---------------------
+
+ABlog v0.8.3 is released to bring you recent enhancements:
+
+ * `ninmesara`_ added ``:nocomments:`` argument to :rst:dir:`post` directive
+ to disable comments per post.
+ * `José Carlos García`_ added Spanish translations.
+
+.. _ninmesara: https://github.com/ninmesara
+.. _José Carlos García: https://github.com/quobit
diff --git a/docs/release/ablog-v0.9-released.rst b/docs/release/ablog-v0.9-released.rst
new file mode 100644
index 0000000..bb7a77f
--- /dev/null
+++ b/docs/release/ablog-v0.9-released.rst
@@ -0,0 +1,61 @@
+ABlog v0.9 released
+===================
+
+.. post:: Feb 17, 2018
+ :author: Nabil Freij
+ :category: Release
+ :location: World
+
+ABlog v0.9.0 is released with the main focus being to support the latest version of Sphinx.
+This also moves the main development from `Abakan`_ to `SunPy`_.
+
+This has merged in all current (at time of writing, 6) open PRs to the original repository.
+
+These are:
+
+`fix(commands): Update command arguments so patterns works correctly <https://github.com/abakan-zz/ablog/pull/96>`__ from `rayalan <https://github.com/rayalan>`__.
+
+`Fix couple of bugs with latest stable Sphinx <https://github.com/abakan-zz/ablog/pull/93>`__ from `tadeboro <https://github.com/tadeboro>`__.
+
+`don't use fancy quotes in the conf.py template <https://github.com/abakan-zz/ablog/pull/87>`__ from `tiwo <https://github.com/tiwo>`__.
+
+`Pass through additional Sphinx options and fix a typo <https://github.com/abakan-zz/ablog/pull/84>`__ from `ahrbel <https://github.com/ahrbe1>`__.
+
+`fix #78 (ImportError: cannot import name make_admonition in Sphinx 1.6) <https://github.com/abakan-zz/ablog/pull/79>`_ from `lsaffre <https://github.com/lsaffre>`__.
+
+`Raise exception when title is missing <https://github.com/abakan-zz/ablog/pull/76>`__ from `rgrinberg <https://github.com/rgrinberg>`__.
+
+.. _Abakan: https://github.com/abakan/ablog
+.. _SunPy: https://github.com/sunpy/ablog
+
+ABlog v0.9.1 released
+---------------------
+
+Minor update to remove Ablog{}.format(python_number) exes
+
+ABlog v0.9.2 released
+---------------------
+
+Fixed Windows String issue.
+
+ABlog v0.9.3 released
+---------------------
+
+Added example on how to use writing blog posts in Jupyter notebooks.
+
+`show </span> when if fa <https://github.com/sunpy/ablog/pull/22>`__, `Add create file encoding <https://github.com/sunpy/ablog/pull/23>`__ and `fix serve command path <https://github.com/sunpy/ablog/pull/31>`__ from `anzawatta <https://github.com/anzawatta>`__.
+
+Sorry I was late to release these!
+
+ABlog v0.9.4 released
+---------------------
+
+Fixes for gettext break and some pathing issues.
+
+ABlog v0.9.5 released
+---------------------
+
+v0.9.5 is the that supports Python 2 and Sphinx <2.
+v0.10.0 on main now, will not.
+
+`Define an auto-orphan option <https://github.com/sunpy/ablog/pull/39>`__, `Repair update directive <https://github.com/sunpy/ablog/pull/37>`__ and `Fix sidebar and blog_baseurl misconfig during quick start <https://github.com/sunpy/ablog/pull/36>`__ from `rayalan <https://github.com/rayalan>`__.
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..2271d48
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,64 @@
+[build-system]
+requires = ["setuptools", "setuptools_scm", "wheel"]
+build-backend = 'setuptools.build_meta'
+
+[tool.black]
+line-length = 120
+include = '\.pyi?$'
+exclude = '''
+(
+ /(
+ \.eggs
+ | \.git
+ | \.mypy_cache
+ | \.tox
+ | \.venv
+ | _build
+ | buck-out
+ | build
+ | dist
+ | astropy_helpers
+ | docs
+ | .history
+ )/
+ | ah_bootstrap.py
+)
+'''
+target-version = ['py310']
+
+[tool.ruff]
+# Enable Pyflakes `E` and `F` codes by default.
+select = ["E", "F"]
+ignore = ["E501", "E741"]
+
+# Allow autofix for all enabled rules (when `--fix`) is provided.
+fixable = ["A", "B", "C", "D", "E", "F"]
+
+exclude = [
+ ".eggs",
+ ".git",
+ ".mypy_cache",
+ ".ruff_cache",
+ ".tox",
+ ".venv",
+ "__pypackages__",
+ "_build",
+ "build",
+ "dist",
+ "node_modules",
+ "venv",
+]
+
+# Same as Black.
+line-length = 120
+target-version = "py310"
+
+# Allow unused variables when underscore-prefixed.
+dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
+
+[tool.codespell]
+skip = "*cache*,*egg*,*extern*,.git,.idea,.tox,*.svg,.history,*sphinx.po"
+ignore-words-list = "THIRDPARTY,"
+
+[tool.djlint]
+files=["*.html"]
diff --git a/roots/test-build/conf.py b/roots/test-build/conf.py
new file mode 100644
index 0000000..a375b9a
--- /dev/null
+++ b/roots/test-build/conf.py
@@ -0,0 +1,16 @@
+extensions = ["ablog"]
+
+# Enable Atom feed generation
+blog_baseurl = "https://blog.example.com/"
+# Include full post in feeds
+blog_feed_fulltext = True
+# Add a social media Atom feed
+blog_feed_templates = {
+ # Use defaults, no templates
+ "atom": {},
+ # Create content text suitable posting to micro-bogging
+ "social": {
+ # Format tags as hashtags and append to the content
+ "content": "{{ title }}{% for tag in post.tags %}" " #{{ tag.name|trim()|replace(' ', '') }}" "{% endfor %}",
+ },
+}
diff --git a/roots/test-build/foo-empty-post.rst b/roots/test-build/foo-empty-post.rst
new file mode 100644
index 0000000..e2221df
--- /dev/null
+++ b/roots/test-build/foo-empty-post.rst
@@ -0,0 +1,5 @@
+.. post:: 2021-03-23
+
+##############
+Foo Empty Post
+##############
diff --git a/roots/test-build/index.rst b/roots/test-build/index.rst
new file mode 100644
index 0000000..b063e42
--- /dev/null
+++ b/roots/test-build/index.rst
@@ -0,0 +1,2 @@
+test-build
+============
diff --git a/roots/test-build/post.rst b/roots/test-build/post.rst
new file mode 100644
index 0000000..46ac292
--- /dev/null
+++ b/roots/test-build/post.rst
@@ -0,0 +1,11 @@
+.. post:: 2022-12-01
+ :tags: Foo Tag, BarTag
+
+Foo Post Title
+==============
+
+ Foo post description `with link`_.
+
+Foo post content.
+
+.. _`with link`: https://example.com
diff --git a/roots/test-canonical/canonical.rst b/roots/test-canonical/canonical.rst
new file mode 100644
index 0000000..c5e706b
--- /dev/null
+++ b/roots/test-canonical/canonical.rst
@@ -0,0 +1,9 @@
+.. post:: 2021-12-01
+ :tags: Canonical
+ :canonical_link: https://canonical.example.org/foo.html
+
+Canonical post
+=============
+
+This post will get generated, but its [canonical link](https://datatracker.ietf.org/doc/html/rfc6596)
+in the header will point to ``canonical_link``.
diff --git a/roots/test-canonical/conf.py b/roots/test-canonical/conf.py
new file mode 100644
index 0000000..a515a6e
--- /dev/null
+++ b/roots/test-canonical/conf.py
@@ -0,0 +1,18 @@
+extensions = ["ablog"]
+
+# Enable Atom feed generation
+blog_baseurl = "https://blog.example.com/"
+# Include full post in feeds
+blog_feed_fulltext = True
+# Add a social media Atom feed
+blog_feed_templates = {
+ # Use defaults, no templates
+ "atom": {},
+ # Create content text suitable posting to micro-bogging
+ "social": {
+ # Format tags as hashtags and append to the content
+ "content": "{{ title }}{% for tag in post.tags %}" " #{{ tag.name|trim()|replace(' ', '') }}" "{% endfor %}",
+ },
+}
+# Sphinx creates canonical links pointing to this base URL by default
+html_baseurl = blog_baseurl
diff --git a/roots/test-canonical/index.rst b/roots/test-canonical/index.rst
new file mode 100644
index 0000000..4a0bb51
--- /dev/null
+++ b/roots/test-canonical/index.rst
@@ -0,0 +1,2 @@
+test-external
+=============
diff --git a/roots/test-canonical/post.rst b/roots/test-canonical/post.rst
new file mode 100644
index 0000000..46ac292
--- /dev/null
+++ b/roots/test-canonical/post.rst
@@ -0,0 +1,11 @@
+.. post:: 2022-12-01
+ :tags: Foo Tag, BarTag
+
+Foo Post Title
+==============
+
+ Foo post description `with link`_.
+
+Foo post content.
+
+.. _`with link`: https://example.com
diff --git a/roots/test-canonical/postlist.rst b/roots/test-canonical/postlist.rst
new file mode 100644
index 0000000..d944b64
--- /dev/null
+++ b/roots/test-canonical/postlist.rst
@@ -0,0 +1,4 @@
+postlist
+========
+
+.. postlist::
diff --git a/roots/test-external/conf.py b/roots/test-external/conf.py
new file mode 100644
index 0000000..a375b9a
--- /dev/null
+++ b/roots/test-external/conf.py
@@ -0,0 +1,16 @@
+extensions = ["ablog"]
+
+# Enable Atom feed generation
+blog_baseurl = "https://blog.example.com/"
+# Include full post in feeds
+blog_feed_fulltext = True
+# Add a social media Atom feed
+blog_feed_templates = {
+ # Use defaults, no templates
+ "atom": {},
+ # Create content text suitable posting to micro-bogging
+ "social": {
+ # Format tags as hashtags and append to the content
+ "content": "{{ title }}{% for tag in post.tags %}" " #{{ tag.name|trim()|replace(' ', '') }}" "{% endfor %}",
+ },
+}
diff --git a/roots/test-external/external.rst b/roots/test-external/external.rst
new file mode 100644
index 0000000..c3ef0f7
--- /dev/null
+++ b/roots/test-external/external.rst
@@ -0,0 +1,8 @@
+.. post:: 2021-12-01
+ :tags: External
+ :external_link: https://www.sphinx-doc.org/en/master/
+
+External post
+=============
+
+This text will be in auto-generated post previews, but links to the post will direct to ``external_link``.
diff --git a/roots/test-external/index.rst b/roots/test-external/index.rst
new file mode 100644
index 0000000..4a0bb51
--- /dev/null
+++ b/roots/test-external/index.rst
@@ -0,0 +1,2 @@
+test-external
+=============
diff --git a/roots/test-external/postlist.rst b/roots/test-external/postlist.rst
new file mode 100644
index 0000000..d944b64
--- /dev/null
+++ b/roots/test-external/postlist.rst
@@ -0,0 +1,4 @@
+postlist
+========
+
+.. postlist::
diff --git a/roots/test-parallel/conf.py b/roots/test-parallel/conf.py
new file mode 100644
index 0000000..9fefc4c
--- /dev/null
+++ b/roots/test-parallel/conf.py
@@ -0,0 +1 @@
+extensions = ["ablog"]
diff --git a/roots/test-parallel/index.rst b/roots/test-parallel/index.rst
new file mode 100644
index 0000000..6512aad
--- /dev/null
+++ b/roots/test-parallel/index.rst
@@ -0,0 +1,7 @@
+test-postlist
+===============
+
+.. toctree::
+ :maxdepth: 1
+
+ postlist
diff --git a/roots/test-parallel/post1.rst b/roots/test-parallel/post1.rst
new file mode 100644
index 0000000..102e2c3
--- /dev/null
+++ b/roots/test-parallel/post1.rst
@@ -0,0 +1,4 @@
+.. post:: 2020-12-01
+
+post 1
+=======
diff --git a/roots/test-parallel/post2.rst b/roots/test-parallel/post2.rst
new file mode 100644
index 0000000..77a7860
--- /dev/null
+++ b/roots/test-parallel/post2.rst
@@ -0,0 +1,4 @@
+.. post:: 2020-12-01
+
+post 2
+=======
diff --git a/roots/test-parallel/post3.rst b/roots/test-parallel/post3.rst
new file mode 100644
index 0000000..903b3da
--- /dev/null
+++ b/roots/test-parallel/post3.rst
@@ -0,0 +1,4 @@
+.. post:: 2020-12-01
+
+post 3
+=======
diff --git a/roots/test-parallel/post4.rst b/roots/test-parallel/post4.rst
new file mode 100644
index 0000000..04f19d1
--- /dev/null
+++ b/roots/test-parallel/post4.rst
@@ -0,0 +1,4 @@
+.. post:: 2020-12-01
+
+post 4
+=======
diff --git a/roots/test-parallel/postlist.rst b/roots/test-parallel/postlist.rst
new file mode 100644
index 0000000..a3d282d
--- /dev/null
+++ b/roots/test-parallel/postlist.rst
@@ -0,0 +1,4 @@
+postlist
+==========
+
+.. postlist::
diff --git a/roots/test-postlist/conf.py b/roots/test-postlist/conf.py
new file mode 100644
index 0000000..9fefc4c
--- /dev/null
+++ b/roots/test-postlist/conf.py
@@ -0,0 +1 @@
+extensions = ["ablog"]
diff --git a/roots/test-postlist/index.rst b/roots/test-postlist/index.rst
new file mode 100644
index 0000000..6512aad
--- /dev/null
+++ b/roots/test-postlist/index.rst
@@ -0,0 +1,7 @@
+test-postlist
+===============
+
+.. toctree::
+ :maxdepth: 1
+
+ postlist
diff --git a/roots/test-postlist/post.rst b/roots/test-postlist/post.rst
new file mode 100644
index 0000000..ce87887
--- /dev/null
+++ b/roots/test-postlist/post.rst
@@ -0,0 +1,4 @@
+.. post:: 2020-12-01
+
+post
+=======
diff --git a/roots/test-postlist/postlist.rst b/roots/test-postlist/postlist.rst
new file mode 100644
index 0000000..a3d282d
--- /dev/null
+++ b/roots/test-postlist/postlist.rst
@@ -0,0 +1,4 @@
+postlist
+==========
+
+.. postlist::
diff --git a/roots/test-templates/_templates/ablog/postcard.html b/roots/test-templates/_templates/ablog/postcard.html
new file mode 100644
index 0000000..834f8dd
--- /dev/null
+++ b/roots/test-templates/_templates/ablog/postcard.html
@@ -0,0 +1 @@
+custom postcard.html
diff --git a/roots/test-templates/_themes/test_theme/ablog/postcard.html b/roots/test-templates/_themes/test_theme/ablog/postcard.html
new file mode 100644
index 0000000..59d4c35
--- /dev/null
+++ b/roots/test-templates/_themes/test_theme/ablog/postcard.html
@@ -0,0 +1 @@
+custom postcard.html from theme
diff --git a/roots/test-templates/_themes/test_theme/theme.toml b/roots/test-templates/_themes/test_theme/theme.toml
new file mode 100644
index 0000000..dae9fb3
--- /dev/null
+++ b/roots/test-templates/_themes/test_theme/theme.toml
@@ -0,0 +1,5 @@
+[theme]
+inherit = "basic"
+
+[options]
+ablog_inject_templates_after_theme = true
diff --git a/roots/test-templates/conf.py b/roots/test-templates/conf.py
new file mode 100644
index 0000000..9fefc4c
--- /dev/null
+++ b/roots/test-templates/conf.py
@@ -0,0 +1 @@
+extensions = ["ablog"]
diff --git a/roots/test-templates/index.rst b/roots/test-templates/index.rst
new file mode 100644
index 0000000..6512aad
--- /dev/null
+++ b/roots/test-templates/index.rst
@@ -0,0 +1,7 @@
+test-postlist
+===============
+
+.. toctree::
+ :maxdepth: 1
+
+ postlist
diff --git a/roots/test-templates/post.rst b/roots/test-templates/post.rst
new file mode 100644
index 0000000..01c64d2
--- /dev/null
+++ b/roots/test-templates/post.rst
@@ -0,0 +1,5 @@
+.. post:: 2020-12-01
+ :author: Durden
+
+post
+=======
diff --git a/roots/test-templates/postlist.rst b/roots/test-templates/postlist.rst
new file mode 100644
index 0000000..a3d282d
--- /dev/null
+++ b/roots/test-templates/postlist.rst
@@ -0,0 +1,4 @@
+postlist
+==========
+
+.. postlist::
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..8b465a7
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,112 @@
+[metadata]
+name = ablog
+author = The SunPy Community
+author_email = sunpy@googlegroups.com
+description = A Sphinx extension that converts any documentation or personal website project into a full-fledged blog.
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+license = MIT
+url = https://ablog.readthedocs.io/
+edit_on_github = True
+github_project = sunpy/ablog
+
+[options]
+python_requires = >=3.10
+package_dir=
+ =src
+packages=find:
+include_package_data = True
+setup_requires =
+ setuptools_scm
+install_requires =
+ docutils>=0.18
+ feedgen>=0.9.0
+ invoke>=1.6.0
+ packaging>=19.0
+ python-dateutil>=2.8.2
+ sphinx>=6.2.0
+ watchdog>=2.1.0
+
+[options.packages.find]
+where=src
+
+[options.extras_require]
+notebook =
+ ipython>=7.30.0
+ nbsphinx>=0.8.0
+markdown =
+ myst-parser>=0.17.0
+docs =
+ alabaster>=1.0.0
+ sphinx-automodapi
+tests =
+ pytest
+ defusedxml>=0.8.0rc2
+
+[options.entry_points]
+console_scripts =
+ ablog = ablog.commands:ablog_main
+
+[tool:pytest]
+testpaths = "tests"
+norecursedirs = ".tox" "build" "docs[\/]_build" "docs[\/]generated" "*.egg-info" ".history"
+markers =
+ sphinx
+addopts = -p no:unraisableexception -p no:threadexception
+filterwarnings =
+ error
+ # Do not fail on pytest config issues (i.e. missing plugins) but do show them
+ always::pytest.PytestConfigWarning
+ # Sphinx and other packages raise these
+ ignore:'imghdr' is deprecated and slated for removal in Python 3.13:DeprecationWarning
+ # python-datetuil
+ ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning
+
+[pycodestyle]
+max_line_length = 120
+
+[flake8]
+max-line-length = 120
+
+[isort]
+default_section = THIRDPARTY
+force_grid_wrap = 0
+include_trailing_comma = true
+known_first_party = ablog
+length_sort = False
+length_sort_sections = stdlib
+line_length = 120
+multi_line_output = 3
+skip = .history
+sections = FUTURE, STDLIB, THIRDPARTY, FIRSTPARTY, LOCALFOLDER
+
+[coverage:run]
+omit =
+ */ablog/__init__*
+ */ablog/*/tests/*
+ */ablog/*setup*
+ */ablog/conftest.py
+ */ablog/cython_version*
+ */ablog/extern/*
+ */ablog/version*
+ ablog/__init__*
+ ablog/*/tests/*
+ ablog/*setup*
+ ablog/conftest.py
+ ablog/cython_version*
+ ablog/extern/*
+ ablog/version*
+
+[coverage:report]
+exclude_lines =
+ # Have to re-enable the standard pragma
+ pragma: no cover
+ # Don't complain about packages we have installed
+ except ImportError
+ # Don't complain if tests don't hit assertions
+ raise AssertionError
+ raise NotImplementedError
+ # Don't complain about script hooks
+ def main\(.*\):
+ # Ignore branches that don't pertain to this version of Python
+ pragma: py{ignore_python_version}
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..24662f3
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+from setuptools import setup # isort:skip
+import os
+from itertools import chain
+
+try:
+ # Recommended for setuptools 61.0.0+
+ # (though may disappear in the future)
+ from setuptools.config.setupcfg import read_configuration
+except ImportError:
+ from setuptools.config import read_configuration
+
+################################################################################
+# Programmatically generate some extras combos.
+################################################################################
+extras = read_configuration("setup.cfg")["options"]["extras_require"]
+
+# Dev is everything
+extras["dev"] = list(chain(*extras.values()))
+
+# All is everything but tests and docs
+exclude_keys = ("tests", "docs", "dev")
+ex_extras = dict(filter(lambda i: i[0] not in exclude_keys, extras.items()))
+# Concatenate all the values together for 'all'
+extras["all"] = list(chain.from_iterable(ex_extras.values()))
+
+setup(
+ extras_require=extras,
+ use_scm_version={"write_to": os.path.join("src", "ablog", "_version.py")},
+)
diff --git a/src/ablog/__init__.py b/src/ablog/__init__.py
new file mode 100755
index 0000000..4b2c428
--- /dev/null
+++ b/src/ablog/__init__.py
@@ -0,0 +1,172 @@
+"""
+ABlog for Sphinx.
+"""
+
+import os
+from glob import glob
+from pathlib import PurePath
+
+from sphinx.builders.html import StandaloneHTMLBuilder
+from sphinx.errors import ThemeError
+from sphinx.jinja2glue import BuiltinTemplateLoader, SphinxFileSystemLoader
+from sphinx.locale import get_translation
+
+from .blog import CONFIG, Blog
+from .post import (
+ CheckFrontMatter,
+ PostDirective,
+ PostListDirective,
+ UpdateDirective,
+ UpdateNode,
+ generate_archive_pages,
+ generate_atom_feeds,
+ missing_reference,
+ process_postlist,
+ process_posts,
+ purge_posts,
+)
+from .version import version as __version__
+
+__all__ = ["setup", "__version__"]
+
+PKGDIR = os.path.abspath(os.path.dirname(__file__))
+# Name used for the *.pot, *.po and *.mo files
+MESSAGE_CATALOG_NAME = "sphinx"
+_ = get_translation(MESSAGE_CATALOG_NAME) # NOQA
+
+
+def get_html_templates_path():
+ """
+ Return path to ABlog templates folder.
+ """
+ pkgdir = os.path.abspath(os.path.dirname(__file__))
+ return os.path.join(pkgdir, "templates")
+
+
+def anchor(post):
+ """
+ Return anchor string for posts that are page sections.
+ """
+ if post.section:
+ return "#" + post.section
+ else:
+ return ""
+
+
+def builder_support(builder):
+ """
+ Return True when builder is supported.
+
+ Supported builders output in html format, but exclude
+ `PickleHTMLBuilder` and `JSONHTMLBuilder`, which run into issues
+ when serializing blog objects.
+ """
+ if hasattr(builder, "builder"):
+ builder = builder.builder
+ not_supported = {"json", "pickle"}
+ return builder.format == "html" and builder.name not in not_supported
+
+
+def html_page_context(app, pagename, templatename, context, doctree):
+ if builder_support(app):
+ context["ablog"] = blog = Blog(app)
+ context["anchor"] = anchor
+ if pagename in blog and blog[pagename].canonical_link:
+ context["pageurl"] = blog[pagename].canonical_link
+ # following is already available for archive pages
+ if blog.blog_baseurl and "feed_path" not in context:
+ context["feed_path"] = blog.blog_path
+ context["feed_title"] = blog.blog_title
+
+
+def config_inited(app, config):
+ # Automatically identify any blog posts if a pattern is specified in the config
+ if isinstance(config.blog_post_pattern, str):
+ config.blog_post_pattern = [config.blog_post_pattern]
+ matched_patterns = []
+ for pattern in config.blog_post_pattern:
+ pattern = os.path.join(app.srcdir, pattern)
+ # make sure that blog post paths have forward slashes even on windows
+ matched_patterns.extend(
+ PurePath(ii).relative_to(app.srcdir).with_suffix("").as_posix() for ii in glob(pattern, recursive=True)
+ )
+ app.config.matched_blog_posts = matched_patterns
+
+ # Add ablog stylesheets to static_path.
+ static_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "stylesheets"))
+ app.config.html_static_path.append(static_path)
+
+
+def builder_inited(app):
+ if not isinstance(app.builder, StandaloneHTMLBuilder) or app.config.skip_injecting_base_ablog_templates:
+ return
+ if not isinstance(app.builder.templates, BuiltinTemplateLoader):
+ raise Exception(
+ "Ablog does not know how to inject templates into with custom "
+ "template bridges. You can use `ablog.get_html_templates_path()` to "
+ "get the path to add in your custom template bridge and set "
+ "`skip_injecting_base_ablog_templates = False` in your "
+ "`conf.py` file."
+ )
+ if get_html_templates_path() in app.config.templates_path:
+ raise Exception(
+ "Found the path from `ablog.get_html_templates_path()` in the "
+ "`templates_path` variable from `conf.py`. Doing so interferes "
+ "with Ablog's ability to stay compatible with Sphinx themes that "
+ "support it out of the box. Please remove `get_html_templates_path` "
+ "from `templates_path` in your `conf.py` to resolve this."
+ )
+ theme = app.builder.theme
+ loaders = app.builder.templates.loaders
+ templatepathlen = app.builder.templates.templatepathlen
+ try:
+ # Modern Sphinx now errors instead of returning the default if there is not a value
+ # in any of the config files.
+ after_theme = theme.get_config("options", "ablog_inject_templates_after_theme", False)
+ except ThemeError:
+ after_theme = False
+ if after_theme:
+ # Inject *after* the user templates and the theme templates,
+ # allowing themes to override the templates provided by this
+ # extension while those templates still serve as a fallback.
+ loaders.append(SphinxFileSystemLoader(get_html_templates_path()))
+ else:
+ # Inject *after* the user templates and *before* the theme
+ # templates. This enables ablog to provide support for themes
+ # that don't support it out-of-the-box, like alabaster.
+ loaders.insert(templatepathlen, SphinxFileSystemLoader(get_html_templates_path()))
+
+
+def setup(app):
+ """
+ Setup ABlog extension.
+ """
+ app.require_sphinx("6.2")
+ for args in CONFIG:
+ app.add_config_value(*args[:3])
+ app.add_directive("post", PostDirective)
+ app.add_directive("postlist", PostListDirective)
+ app.connect("config-inited", config_inited)
+ app.connect("builder-inited", builder_inited)
+ app.connect("doctree-read", process_posts)
+ app.connect("env-purge-doc", purge_posts)
+ app.connect("doctree-resolved", process_postlist)
+ app.connect("missing-reference", missing_reference)
+ app.connect("html-collect-pages", generate_archive_pages)
+ app.connect("html-collect-pages", generate_atom_feeds)
+ app.connect("html-page-context", html_page_context)
+ app.add_transform(CheckFrontMatter)
+ app.add_directive("update", UpdateDirective)
+ app.add_node(
+ UpdateNode,
+ html=(lambda s, n: s.visit_admonition(n), lambda s, n: s.depart_admonition(n)),
+ latex=(lambda s, n: s.visit_admonition(n), lambda s, n: s.depart_admonition(n)),
+ )
+ pkgdir = os.path.abspath(os.path.dirname(__file__))
+ locale_dir = os.path.join(pkgdir, "locales")
+ app.add_message_catalog(MESSAGE_CATALOG_NAME, locale_dir)
+ return {
+ "version": __version__,
+ "parallel_read_safe": False,
+ "parallel_write_safe": True,
+ }
diff --git a/src/ablog/blog.py b/src/ablog/blog.py
new file mode 100644
index 0000000..afadd70
--- /dev/null
+++ b/src/ablog/blog.py
@@ -0,0 +1,615 @@
+"""
+Classes for handling posts and archives.
+"""
+
+import os
+import re
+import datetime as dtmod
+from datetime import datetime
+from operator import attrgetter
+from unicodedata import normalize
+from urllib.parse import urljoin
+from collections.abc import Container
+
+from docutils import nodes
+from docutils.io import StringOutput
+from docutils.utils import new_document
+from sphinx import addnodes
+from sphinx.util.osutil import relative_uri
+
+__all__ = ["Blog", "Post", "Collection", "BlogPageMixin", "Catalog"]
+
+
+def slugify(string):
+ """
+ Slugify *s*.
+ """
+ string = normalize("NFKD", str(string))
+ string = re.sub(r"[^\w\s-]", "", string).strip().lower()
+ return re.sub(r"[-\s]+", "-", string)
+
+
+def os_path_join(path, *paths):
+ return os.path.join(path, *paths).replace(os.path.sep, "/")
+
+
+def require_config_type(type_, is_optional=True):
+ def verify_fn(key, value, config):
+ if isinstance(value, type_) or (is_optional and value is None):
+ return value
+ # Historically, we're pretty sloppy on whether None or False is the default for omission
+ # so accept them both.
+ if value is False and is_optional:
+ return None
+ raise KeyError(key + " must be a " + type_.__name__ + (" or omitted" if is_optional else ""))
+
+ return verify_fn
+
+
+def require_config_str_or_list_lookup(lookup_config_key, is_optional=True):
+ """
+ The default values can be a string or list of strings that match entries in
+ a comprehensive list -- for example, the default authors are one or more of
+ all the authors.
+ """
+
+ def verify_fn(key, value, config):
+ if is_optional and value is None:
+ return value
+ if isinstance(value, str):
+ value = [value]
+ if not isinstance(value, list):
+ raise KeyError(key + " must be a str or list")
+ allowed_values = config[lookup_config_key]
+ for v in value:
+ if v not in allowed_values:
+ raise KeyError(str(v) + "must be a key of " + lookup_config_key)
+ return value
+
+ return verify_fn
+
+
+def require_config_full_name_link_dict(is_link_optional=True):
+ """
+ The definition for authors and similar entries is to map a short name to a
+ (full name, link) tuple.
+ """
+
+ def verify_fn(key, value, config):
+ for full_name, link in value.values():
+ if not isinstance(full_name, str):
+ raise KeyError(key + " must have full name entries that are strings")
+ is_link_valid = isinstance(link, str) or (is_link_optional and link is None)
+ if not is_link_valid:
+ raise KeyError(key + " links must be a string" + (" or omitted" if is_link_optional else ""))
+ return value
+
+ return verify_fn
+
+
+DEBUG = True
+CONFIG = [
+ # name, default, rebuild, verify_fn
+ # where verify_fn is (key, value, app.config) --> value, throwing a KeyError if the value isn't right
+ ("blog_archive_titles", None, False, require_config_type(bool)),
+ ("blog_authors", {}, True, require_config_full_name_link_dict()),
+ ("blog_baseurl", "", True, require_config_type(str)),
+ ("blog_default_author", None, True, require_config_str_or_list_lookup("blog_authors")),
+ ("blog_default_language", None, True, require_config_str_or_list_lookup("blog_languages")),
+ ("blog_default_location", None, True, require_config_str_or_list_lookup("blog_locations")),
+ ("blog_feed_archives", False, True),
+ ("blog_feed_fulltext", False, True),
+ ("blog_feed_length", None, None),
+ ("blog_feed_subtitle", None, True),
+ ("blog_feed_templates", {"atom": {}}, True),
+ ("blog_feed_titles", None, False),
+ ("blog_languages", {}, True, require_config_full_name_link_dict()),
+ ("blog_locations", {}, True, require_config_full_name_link_dict()),
+ ("blog_path", "blog", True, require_config_type(str)),
+ ("blog_post_pattern", [], True, require_config_type((str, list))),
+ ("blog_title", "Blog", True, require_config_type(str)),
+ ("disqus_drafts", False, True),
+ ("disqus_pages", False, True),
+ ("disqus_shortname", None, True),
+ ("fontawesome_css_file", "", True, require_config_type(str)),
+ ("fontawesome_included", False, True, require_config_type(bool)),
+ ("fontawesome_link_cdn", None, True),
+ ("post_always_section", False, True),
+ ("post_auto_excerpt", 1, True),
+ ("post_auto_image", 0, True),
+ ("post_auto_orphan", True, True, require_config_type(bool)),
+ ("post_date_format_short", "%d %B", True, require_config_type(str)),
+ ("post_date_format", "%d %B %Y", True, require_config_type(str)),
+ ("post_redirect_refresh", 5, True),
+ ("post_show_prev_next", True, True),
+ ("skip_injecting_base_ablog_templates", False, True),
+]
+TOMORROW = datetime.today() + dtmod.timedelta(1)
+TOMORROW = TOMORROW.replace(hour=0, minute=0, second=0, microsecond=0)
+FUTURE = datetime(9999, 12, 31)
+
+
+def revise_pending_xrefs(doctree, docname):
+ for node in doctree.findall(addnodes.pending_xref):
+ node["refdoc"] = docname
+
+
+def link_posts(posts):
+ """
+ Link posts after sorting them post by published date.
+ """
+ posts = filter(attrgetter("order"), posts)
+ posts = sorted(posts)
+ posts[0].prev = posts[-1].next = None
+ for i in range(1, len(posts)):
+ post = posts[i]
+ posts[i - 1].next = post
+ post.prev = posts[i - 1]
+
+
+class Blog(Container):
+ """
+ Handle blog operations.
+ """
+
+ # using a shared state
+ _dict = {}
+
+ def __init__(self, app):
+ self.__dict__ = self._dict
+ if not self._dict:
+ self._init(app)
+
+ def _init(self, app):
+ """
+ Instantiate Blog.
+ """
+ self.app = app
+ self.config = {}
+ self.references = refs = {}
+ # get configuration from Sphinx app
+ for opt in CONFIG:
+ try:
+ key, _, _, verify_fn = opt
+ except ValueError:
+ key, _, _ = opt
+ verify_fn = None
+ value = verify_fn(key, getattr(app.config, key), app.config) if verify_fn else getattr(app.config, opt[0])
+ self.config[opt[0]] = value
+ # blog catalog contains all posts
+ self.blog = Catalog(self, "blog", "blog", None)
+ # contains post collections by year
+ self.archive = Catalog(self, "archive", "archive", None, reverse=True)
+ self.archive.docname += "/archive"
+ refs["blog-archives"] = (self.archive.docname, "Archives")
+ self.catalogs = cat = {} # catalogs of user set labels
+ self.tags = cat["tags"] = Catalog(self, "tags", "tag", "tag")
+ refs["blog-tags"] = (self.tags.docname, "Tags")
+ self.author = cat["author"] = Catalog(self, "author", "author", "author")
+ refs["blog-authors"] = (self.author.docname, "Authors")
+ self.location = cat["location"] = Catalog(self, "location", "location", "location")
+ refs["blog-locations"] = (self.location.docname, "Locations")
+ self.language = cat["language"] = Catalog(self, "language", "language", "language")
+ refs["blog-languages"] = (self.language.docname, "Languages")
+ self.category = cat["category"] = Catalog(self, "category", "category", "category")
+ refs["blog-categories"] = (self.category.docname, "Categories")
+ for catname in ["author", "location", "language"]:
+ catalog = self.catalogs[catname]
+ items = self.config["blog_" + catname + "s"].items()
+ for label, (name, link) in items:
+ catalog[label] = Collection(catalog, label, name, link)
+ self.posts = self.blog["post"] = Collection(self.blog, "post", "Posts", path=self.blog_path)
+ self.drafts = self.blog["draft"] = Collection(
+ self.blog, "draft", "Drafts", path=os_path_join(self.blog_path, "drafts")
+ )
+ # add references to posts and drafts
+ # e.g. :ref:`blog-posts`
+ refs["blog-posts"] = (self.config["blog_path"], "Posts")
+ refs["blog-drafts"] = (os_path_join(self.config["blog_path"], "drafts"), "Drafts")
+ refs["blog-feed"] = (os_path_join(self.config["blog_path"], "atom.xml"), self.blog_title + " Feed")
+ # set some internal configuration options
+ self.config["fontawesome"] = (
+ self.config["fontawesome_included"]
+ or self.config["fontawesome_link_cdn"]
+ or self.config["fontawesome_css_file"]
+ )
+
+ def __getattr__(self, name):
+ try:
+ attr = self.config[name]
+ except KeyError:
+ raise AttributeError(f"ABlog has no configuration option {repr(name)}")
+ return attr
+
+ def __getitem__(self, key):
+ return self.posts[key] or self.drafts[key]
+
+ def __contains__(self, item):
+ return item in self.posts or item in self.drafts
+
+ def __len__(self):
+ return len(self.posts)
+
+ def __nonzero__(self):
+ return len(self) > 0
+
+ @property
+ def feed_path(self):
+ """
+ RSS feed page name.
+ """
+ return os_path_join(self.blog_path, "atom.xml")
+
+ def register(self, docname, info):
+ """
+ Register post *docname*.
+ """
+ post = Post(self, docname, info)
+ if post.date and post.date < TOMORROW:
+ self.posts.add(post)
+ else:
+ self.drafts.add(post)
+ for catalog in self.catalogs.values():
+ catalog.add(post)
+
+ def recent(self, num, docname=None, **labels):
+ """
+ Yield *num* recent posts, excluding the one with `docname`.
+ """
+ if num is None:
+ num = len(self)
+ for i, post in enumerate(self.posts):
+ if post.docname == docname:
+ num += 1
+ continue
+ if i == num:
+ return
+ yield post
+
+ def page_id(self, pagename):
+ """
+ Return pagename, trimming :file:`index` from end when found.
+
+ Return value is used as disqus page identifier.
+ """
+ if self.config["blog_baseurl"]:
+ if pagename.endswith("index"):
+ pagename = pagename[:-5]
+ pagename = pagename.strip("/")
+ return "/" + pagename + ("/" if pagename else "")
+
+ def page_url(self, pagename):
+ """
+ Return page URL when :confval:`blog_baseurl` is set, otherwise
+ ``None``.
+
+ When found, :file:`index.html` is trimmed from the end of the
+ URL.
+ """
+ if self.config["blog_baseurl"]:
+ url = urljoin(self.config["blog_baseurl"], pagename)
+ if url.endswith("index"):
+ url = url[:-5]
+ return url
+
+
+def html_builder_write_doc(self, docname, doctree, img_url=False):
+ """
+ Part of :meth:`sphinx.builders.html.StandaloneHTMLBuilder.write_doc` method
+ used to convert *doctree* to HTML.
+
+ Extra argument `img_url` enables conversion of `<img>` source paths to
+ fully qualified URLs based on `blog_baseurl`.
+ """
+ # Source of images
+ img_folder = "_images"
+ if img_url and self.config["blog_baseurl"]:
+ img_src_path = urljoin(self.config["blog_baseurl"], img_folder)
+ else:
+ img_src_path = relative_uri(self.get_target_uri(docname), img_folder)
+
+ destination = StringOutput(encoding="utf-8")
+ doctree.settings = self.docsettings
+ self.secnumbers = {}
+ self.imgpath = img_src_path
+ self.dlpath = relative_uri(self.get_target_uri(docname), "_downloads")
+ self.current_docname = docname
+ self.docwriter.write(doctree, destination)
+ self.docwriter.assemble_parts()
+ return self.docwriter.parts["fragment"]
+
+
+class BlogPageMixin:
+ def __str__(self):
+ return self.title
+
+ def __repr__(self):
+ return str(self) + " <" + str(self.docname) + ">"
+
+ @property
+ def blog(self):
+ """
+ Reference to :class:`~ablog.blog.Blog` object.
+ """
+ return self._blog
+
+ @property
+ def title(self):
+ return getattr(self, "name", getattr(self, "_title"))
+
+
+class Post(BlogPageMixin):
+ """
+ Handle post metadata.
+ """
+
+ def __init__(self, blog, docname, info):
+ self._blog = blog
+ self.docname = docname
+ self.section = info["section"]
+ self.order = info["order"]
+ self.date = date = info["date"]
+ self.update = info["update"]
+ self.nocomments = info["nocomments"]
+ self.published = date and date < TOMORROW
+ self.draft = not self.published
+ self.canonical_link = info["canonical_link"]
+ self.external_link = info["external_link"]
+ self._title = info["title"]
+ self.excerpt = info["excerpt"]
+ self.doctree = info["doctree"]
+ self._next = self._prev = -1
+ self._computed_date = date or FUTURE
+ # archives
+ if self.published:
+ self.tags = info.get("tags")
+ self.author = info.get("author")
+ self.category = info.get("category")
+ self.location = info.get("location")
+ self.language = info.get("language")
+ if not self.author and blog.blog_default_author:
+ self.author = blog.blog_default_author
+ if not self.location and blog.blog_default_location:
+ self.location = blog.blog_default_location
+ if not self.language and blog.blog_default_language:
+ self.language = blog.blog_default_language
+ self.archive = self._blog.archive[self.date.year]
+ self.archive.add(self)
+ else:
+ self.tags = info.get("tags")
+ self.author = info.get("author")
+ self.category = info.get("category")
+ self.location = info.get("location")
+ self.language = info.get("language")
+ self.archive = []
+ self.redirect = info.get("redirect")
+ self.options = info
+
+ def __lt__(self, other):
+ return (self._computed_date, self.title) < (other._computed_date, other.title)
+
+ def to_html(self, pagename, fulltext=False, drop_h1=True, img_url=False):
+ """
+ Return excerpt or *fulltext* as HTML after resolving references with
+ respect to *pagename*.
+
+ By default, first `<h1>` tag is dropped from the output. More
+ than one can be dropped by setting *drop_h1* to the desired
+ number of tags to be dropped.
+
+ `img_url` enables conversion of `<img>` source paths to fully
+ qualified URLs based on `blog_baseurl`.
+ """
+
+ doctree = new_document("")
+ if fulltext:
+ deepcopy = self.doctree.deepcopy()
+ if isinstance(deepcopy, nodes.document):
+ doctree.extend(deepcopy.children)
+ else:
+ doctree.append(deepcopy)
+ else:
+ excerpt_container = nodes.paragraph()
+ excerpt_container.attributes["classes"].append("ablog-post-excerpt")
+ for node in self.excerpt:
+ excerpt_container.append(node.deepcopy())
+ doctree.append(excerpt_container)
+ app = self._blog.app
+ revise_pending_xrefs(doctree, pagename)
+ app.env.resolve_references(doctree, pagename, app.builder)
+ add_permalinks, app.builder.add_permalinks = (app.builder.add_permalinks, False)
+ html = html_builder_write_doc(app.builder, pagename, doctree, img_url=img_url)
+ app.builder.add_permalinks = add_permalinks
+ if drop_h1:
+ html = re.sub("<h1>(.*?)</h1>", "", html, count=abs(int(drop_h1)))
+ return html
+
+ @property
+ def next(self):
+ """
+ Next published post in chronological order.
+ """
+ if self._next == -1:
+ link_posts(self._blog.posts)
+ return self._next
+
+ @next.setter
+ def next(self, post):
+ """
+ Set next published post in chronological order.
+ """
+ self._next = post
+
+ @property
+ def prev(self):
+ """
+ Previous published post in chronological order.
+ """
+ if self._prev == -1:
+ link_posts(self._blog.posts)
+ return self._prev
+
+ @prev.setter
+ def prev(self, post):
+ """
+ Set previous published post in chronological order.
+ """
+ self._prev = post
+
+
+class Catalog(BlogPageMixin):
+ """
+ Handles collections of posts.
+ """
+
+ def __init__(self, blog, name, xref, path, reverse=False):
+ self._blog = blog
+ self.name = name
+ self.xref = xref # for creating labels, e.g. `tag-python`
+ self.collections = {}
+ if path:
+ self.path = self.docname = os_path_join(blog.blog_path, path)
+ else:
+ self.path = self.docname = blog.blog_path
+ self._coll_lens = None
+ self._min_max = None
+ self._reverse = reverse
+
+ def __str__(self):
+ return str(self.name)
+
+ def __getitem__(self, name):
+ try:
+ return self.collections[name]
+ except KeyError:
+ return self.collections.setdefault(name, Collection(self, name))
+
+ def __setitem__(self, name, item):
+ self.collections[name] = item
+
+ def __len__(self):
+ return sum(len(coll) for coll in self)
+
+ def __nonzero__(self):
+ return len(self) > 0
+
+ def __iter__(self):
+ keys = list(self.collections)
+ keys.sort(reverse=self._reverse)
+ for key in keys:
+ yield self.collections[key]
+
+ def add(self, post):
+ """
+ Add post to appropriate collection(s) and replace collections labels
+ with collection objects.
+ """
+ colls = []
+ for label in getattr(post, self.name, []):
+ coll = self[label]
+ if post.published:
+ coll.add(post)
+ colls.append(coll)
+ setattr(post, self.name, colls)
+
+ def _minmax(self):
+ """
+ Return minimum and maximum sizes of collections.
+ """
+ if self._coll_lens is None or len(self._coll_lens) != len(self.collections):
+ self._coll_lens = [len(coll) for coll in self.collections.values() if len(coll)]
+ self._min_max = min(self._coll_lens), max(self._coll_lens)
+ return self._min_max
+
+
+class Collection(BlogPageMixin):
+ """
+ Posts sharing a label, i.e. tag, category, author, or location.
+ """
+
+ def __init__(self, catalog, label, name=None, href=None, path=None, page=0):
+ self._catalog = catalog
+ self._blog = catalog.blog
+ self.label = label
+ self.name = name or self.label
+ self.href = href
+ self.page = page
+ self._posts = {}
+ self._posts_iter = None
+ self._path = path
+ self.xref = self.catalog.xref + "-" + slugify(label)
+ self._slug = None
+ self._html = None
+ self._catalog.blog.references[self.xref] = (self.docname, self.name)
+
+ def __str__(self):
+ return str(self.name)
+
+ def __len__(self):
+ return len(self._posts)
+
+ def __nonzero__(self):
+ return len(self) > 0
+
+ def __unicode__(self):
+ return str(self.name)
+
+ def __iter__(self):
+ if self._posts_iter is None:
+ posts = list(self._posts.values())
+ posts.sort(reverse=True)
+ self._posts_iter = posts
+ yield from self._posts_iter
+
+ def __getitem__(self, key):
+ return self._posts.get(key)
+
+ def __contains__(self, item):
+ return item in self._posts
+
+ def __eq__(self, other):
+ return self.name == other.name
+
+ def __lt__(self, other):
+ return self.name < other.name
+
+ def __gt__(self, other):
+ return self.name > other.name
+
+ @property
+ def catalog(self):
+ """
+ :class:`~ablog.blog.Catalog` that the collection belongs to.
+ """
+ return self._catalog
+
+ def add(self, post):
+ """
+ Add post to the collection.
+ """
+ post_name = post.docname
+ if post.section:
+ post_name += "#" + post.section
+ self._posts[post_name] = post
+
+ def relsize(self, maxsize=5, minsize=1):
+ """
+ Relative size used in tag clouds.
+ """
+ min_, max_ = self.catalog._minmax()
+ diff = maxsize - minsize
+ if len(self.catalog) == 1 or min_ == max_:
+ return int(round(diff / 2.0 + minsize))
+ size = int(1.0 * (len(self) - min_) / (max_ - min_) * diff + minsize)
+ return size
+
+ @property
+ def docname(self):
+ """
+ Collection page document name.
+ """
+ if self._path is None:
+ self._path = os_path_join(self.catalog.path, slugify(self.name))
+ return self._path
+
+ path = docname
diff --git a/src/ablog/commands.py b/src/ablog/commands.py
new file mode 100644
index 0000000..cabb942
--- /dev/null
+++ b/src/ablog/commands.py
@@ -0,0 +1,469 @@
+import os
+import sys
+import glob
+import shutil
+import argparse
+import webbrowser
+import socketserver
+from os import path
+from http import server
+from os.path import join, isfile, abspath
+from datetime import date
+
+from invoke import run
+from watchdog.observers import Observer
+from watchdog.tricks import ShellCommandTrick
+
+import ablog
+from ablog.start import ablog_start
+
+__all__ = ["ablog_build", "ablog_clean", "ablog_serve", "ablog_deploy", "ablog_main"]
+
+BUILDDIR = "_website"
+DOCTREES = ".doctrees"
+POST_TEMPLATE = """
+{title}
+{equal}
+
+.. post:: {date}
+ :tags:
+ :category:
+
+"""
+
+
+def find_confdir(sourcedir=None):
+ """
+ Return path to current directory or its parent that contains conf.py.
+ """
+ confdir = sourcedir or os.getcwd()
+
+ def parent(d):
+ return abspath(join(d, ".."))
+
+ while not isfile(join(confdir, "conf.py")) and confdir != parent(confdir):
+ confdir = parent(confdir)
+ conf = join(confdir, "conf.py")
+ if isfile(conf) and "ablog" in open(conf).read():
+ return confdir
+ else:
+ sys.exit("Current directory and its parents doesn't " "contain configuration file (conf.py).")
+
+
+def read_conf(confdir):
+ """
+ Return conf.py file as a module.
+ """
+ sys.path.insert(0, confdir)
+ conf = __import__("conf")
+ sys.path.pop(0)
+ return conf
+
+
+parser = argparse.ArgumentParser(
+ description="ABlog for blogging with Sphinx",
+ epilog="See 'ablog <command> -h' for more information on a specific " "command.",
+)
+parser.add_argument("-v", "--version", help="print ABlog version and exit", action="version", version=ablog.__version__)
+commands = ablog_commands = parser.add_subparsers(title="commands")
+
+
+def cmd(func=None, **kwargs):
+ if func is None:
+
+ def cmd_inner(func):
+ return cmd(func, **kwargs)
+
+ return cmd_inner
+ else:
+ command = commands.add_parser(**kwargs)
+ command.set_defaults(func=func)
+ command.set_defaults(subparser=command)
+ func.command = command
+ return func
+
+
+def arg(*args, **kwargs):
+ if args and callable(args[0]):
+ func = args[0]
+ args = args[1:]
+ else:
+ func = None
+ if func is None:
+
+ def arg_inner(func):
+ return arg(func, *args, **kwargs)
+
+ return arg_inner
+ else:
+ func.command.add_argument(*args, **kwargs)
+ return func
+
+
+def arg_website(func):
+ arg(
+ func,
+ "-w",
+ dest="website",
+ type=str,
+ help=f"path for website, default is {BUILDDIR} when `ablog_website` is not set in conf.py",
+ )
+ return func
+
+
+def arg_doctrees(func):
+ arg(
+ func,
+ "-d",
+ dest="doctrees",
+ type=str,
+ help="path for the cached environment and doctree files, "
+ f"default {DOCTREES} when `ablog_doctrees` is not set in conf.py",
+ )
+ return func
+
+
+cmd(
+ ablog_start,
+ name="start",
+ help="start a new blog project",
+ description="Start a new blog project by answering a few questions. "
+ "You will end up with a configuration file and sample pages.",
+)
+
+
+@arg("-P", dest="runpdb", action="store_true", default=False, help="run pdb on exception")
+@arg("-T", dest="traceback", action="store_true", default=False, help="show full traceback on exception")
+@arg("-W", dest="werror", action="store_true", default=False, help="turn warnings into errors")
+@arg("-N", dest="no_colors", action="store_true", default=False, help="do not emit colored output")
+@arg("-Q", dest="extra_quiet", action="store_true", default=False, help="no output at all, not even warnings")
+@arg(
+ "-q",
+ dest="quiet",
+ action="store_true",
+ default=False,
+ help="no output on stdout, just warnings on stderr",
+)
+@arg("-v", dest="verbosity", action="count", default=0, help="increase verbosity (can be repeated)")
+@arg_doctrees
+@arg_website
+@arg(
+ "-s",
+ dest="sourcedir",
+ type=str,
+ help="root path for source files, " "default is path to the folder that contains conf.py",
+)
+@arg("-b", dest="builder", type=str, help="builder to use, default `ablog_builder` or dirhtml")
+@arg(
+ "-a",
+ dest="allfiles",
+ action="store_true",
+ default=False,
+ help="write all files; default is to only write new and changed files",
+)
+@cmd(
+ name="build",
+ help="build your blog project",
+ description="Path options can be set in conf.py. " "Default values of paths are relative to conf.py.",
+)
+def ablog_build(
+ builder=None,
+ sourcedir=None,
+ website=None,
+ doctrees=None,
+ traceback=False,
+ runpdb=False,
+ allfiles=False,
+ werror=False,
+ verbosity=0,
+ quiet=False,
+ extra_quiet=False,
+ no_colors=False,
+ **kwargs,
+):
+ confdir = find_confdir(sourcedir)
+ conf = read_conf(confdir)
+ website = website or os.path.join(confdir, getattr(conf, "ablog_website", BUILDDIR))
+ doctrees = doctrees or os.path.join(confdir, getattr(conf, "ablog_doctrees", DOCTREES))
+ sourcedir = sourcedir or confdir
+ argv = sys.argv[:1]
+ argv.extend(["-b", builder or getattr(conf, "ablog_builder", "dirhtml")])
+ argv.extend(["-d", doctrees])
+ if traceback:
+ argv.extend(["-T"])
+ if runpdb:
+ argv.extend(["-P"])
+ if allfiles:
+ argv.extend(["-a"])
+ if werror:
+ argv.extend(["-W"])
+ if verbosity > 0:
+ argv.extend(["-v"] * verbosity)
+ if quiet:
+ argv.extend(["-q"])
+ if extra_quiet:
+ argv.extend(["-Q"])
+ if no_colors:
+ argv.extend(["-N"])
+ argv.extend([sourcedir, website])
+
+ from sphinx.cmd.build import main
+
+ sys.exit(main(argv[1:]))
+
+
+@arg(
+ "-D",
+ dest="deep",
+ action="store_true",
+ default=False,
+ help="deep clean, remove cached environment and doctree files",
+)
+@arg_doctrees
+@arg_website
+@cmd(
+ name="clean",
+ help="clean your blog build files",
+ description="Path options can be set in conf.py. " "Default values of paths are relative to conf.py.",
+)
+def ablog_clean(website=None, doctrees=None, deep=False, **kwargs):
+ confdir = find_confdir()
+ conf = read_conf(confdir)
+ website = website or os.path.join(confdir, getattr(conf, "ablog_website", BUILDDIR))
+ doctrees = doctrees or os.path.join(confdir, getattr(conf, "ablog_doctrees", DOCTREES))
+ nothing = True
+ if glob.glob(os.path.join(website, "*")):
+ shutil.rmtree(website)
+ print(f"Removed {os.path.relpath(website)}.")
+ nothing = False
+ if deep and glob.glob(os.path.join(doctrees, "*")):
+ shutil.rmtree(doctrees)
+ print(f"Removed {os.path.relpath(doctrees)}.")
+ nothing = False
+ if nothing:
+ print("Nothing to clean.")
+
+
+@arg("--patterns", dest="patterns", default="*.rst;*.txt", help="patterns for triggering rebuilds")
+@arg(
+ "-r",
+ dest="rebuild",
+ action="store_true",
+ default=False,
+ help="rebuild when a file matching patterns change or get added",
+)
+@arg("-n", dest="view", action="store_false", default=True, help="do not open website in a new browser tab")
+@arg("-p", dest="port", type=int, default=8000, help="port number for HTTP server; default is 8000")
+@arg_website
+@cmd(
+ name="serve",
+ help="serve and view your project",
+ description="Serve options can be set in conf.py. " "Default values of paths are relative to conf.py.",
+)
+def ablog_serve(website=None, port=8000, view=True, rebuild=False, patterns="*.rst;*.txt", **kwargs):
+ confdir = find_confdir()
+ conf = read_conf(confdir)
+ # to allow restarting the server in short succession
+ socketserver.TCPServer.allow_reuse_address = True
+ Handler = server.SimpleHTTPRequestHandler
+ httpd = socketserver.TCPServer(("", port), Handler)
+ ip, port = httpd.socket.getsockname()
+ print(f"Serving HTTP on {ip}:{port}.")
+ print("Quit the server with Control-C.")
+ website = website or os.path.join(confdir, getattr(conf, "ablog_website", "_website"))
+ os.chdir(website)
+ if rebuild:
+ patterns = patterns.split(";")
+ ignore_patterns = [os.path.join(website, "*")]
+ handler = ShellCommandTrick(
+ shell_command="ablog build -s " + confdir,
+ patterns=patterns,
+ ignore_patterns=ignore_patterns,
+ ignore_directories=False,
+ wait_for_process=True,
+ drop_during_process=False,
+ )
+ observer = Observer(timeout=1)
+ observer.schedule(handler, confdir, recursive=True)
+ observer.start()
+ try:
+ if view:
+ webbrowser.open_new_tab(f"http://127.0.0.1:{port}") and httpd.serve_forever()
+ else:
+ httpd.serve_forever()
+ except KeyboardInterrupt:
+ observer.stop()
+ observer.join()
+ else:
+ if view:
+ webbrowser.open_new_tab(f"http://127.0.0.1:{port}") and httpd.serve_forever()
+ else:
+ httpd.serve_forever()
+
+
+@arg("-t", dest="title", type=str, help="post title; default is formed from filename")
+@arg(dest="filename", type=str, help="filename, e.g. my-nth-post (.rst appended)")
+@cmd(name="post", help="create a blank post")
+def ablog_post(filename, title=None, **kwargs):
+ # Generate basic post params.
+ today = date.today()
+ if not filename.lower().endswith(".rst"):
+ filename += ".rst"
+ today = today.strftime("%b %d, %Y")
+ if not title:
+ title = filename[:-4].replace("-", " ").title()
+ pars = {"date": today, "title": title, "equal": "=" * len(title)}
+ if path.isfile(filename):
+ pass
+ else:
+ # read the file, and add post directive
+ # and save it
+ with open(filename, "w", encoding="utf-8") as out:
+ post_text = POST_TEMPLATE.format(**pars)
+ out.write(post_text)
+ print(f"Blog post created: {filename}")
+
+
+@arg(
+ "--github-url",
+ dest="github_url",
+ type=str,
+ default="git@github.com",
+ help="Custom GitHub URL. Useful when multiple accounts are configured "
+ "on the same machine. Default is: git@github.com",
+)
+@arg(
+ "--github-token",
+ dest="github_token",
+ type=str,
+ help="environment variable name storing GitHub access token",
+)
+@arg("--github-ssh", dest="github_is_http", action="store_true", help="use ssh when cloning website")
+@arg(
+ "--github-branch",
+ dest="github_branch",
+ type=str,
+ help="Branch to use. Default is: master.",
+ default="master",
+)
+@arg(
+ "--push-quietly",
+ dest="push_quietly",
+ action="store_true",
+ default=False,
+ help="be more quiet when pushing changes",
+)
+@arg(
+ "-f",
+ dest="push_force",
+ action="store_true",
+ default=False,
+ help="overwrite last commit, i.e. `commit --amend; push -f`",
+)
+@arg("-m", dest="message", type=str, help="commit message")
+@arg("-g", dest="github_pages", type=str, help="GitHub username for deploying to GitHub pages")
+@arg(
+ "-p",
+ dest="repodir",
+ type=str,
+ help="path to the location of repository to be deployed, e.g. "
+ "`../username.github.io`, default is folder containing `conf.py`",
+)
+@arg_website
+@cmd(
+ name="deploy",
+ help="deploy your website build files",
+ description="Path options can be set in conf.py. " "Default values of paths are relative to conf.py.",
+)
+def ablog_deploy(
+ website,
+ message=None,
+ github_pages=None,
+ push_quietly=False,
+ push_force=False,
+ github_token=None,
+ github_is_http=True,
+ github_url=None,
+ github_branch=None,
+ repodir=None,
+ **kwargs,
+):
+ confdir = find_confdir()
+ conf = read_conf(confdir)
+ github_pages = github_pages or getattr(conf, "github_pages", None)
+ github_url = github_url or getattr(conf, "github_url", None)
+ github_url += ":"
+ github_branch = github_branch or getattr(conf, "github_branch", "master")
+ website = website or os.path.join(confdir, getattr(conf, "ablog_builddir", "_website"))
+ tomove = glob.glob(os.path.join(website, "*"))
+ if not tomove:
+ print("Nothing to deploy, build first.")
+ return
+ if github_pages:
+ if repodir is None:
+ repodir = os.path.join(confdir, f"{github_pages}.github.io")
+ if os.path.isdir(repodir):
+ os.chdir(repodir)
+ run("git pull", echo=True)
+ else:
+ run(
+ "git clone "
+ + ("https://github.com/" if github_is_http else github_url)
+ + "{0}/{0}.github.io.git {1}".format(github_pages, repodir),
+ echo=True,
+ )
+ git_add = []
+ for tm in tomove:
+ for root, dirnames, filenames in os.walk(website):
+ for filename in filenames:
+ fn = os.path.join(root, filename)
+ fnnew = fn.replace(website, repodir)
+ try:
+ os.renames(fn, fnnew)
+ except OSError:
+ if os.path.isdir(fnnew):
+ shutil.rmtree(fnnew)
+ else:
+ os.remove(fnnew)
+ os.renames(fn, fnnew)
+
+ git_add.append(fnnew)
+ print(f"Moved {len(git_add)} files to {github_pages}.github.io")
+ os.chdir(repodir)
+ run("git add -f " + " ".join(['"{}"'.format(os.path.relpath(p)) for p in git_add]), echo=True)
+ if not os.path.isfile(".nojekyll"):
+ open(".nojekyll", "w")
+ run("git add -f .nojekyll")
+ # Check to see if anything has actually been committed
+ result = run("git diff --cached --name-status HEAD")
+ if not result.stdout:
+ print("Nothing changed from last deployment")
+ return
+ commit = f"git commit -m \"{message or 'Updates.'}\""
+ if push_force:
+ commit += " --amend"
+ run(commit, echo=True)
+ if github_token:
+ with open(os.path.join(repodir, ".git/credentials"), "w") as out:
+ out.write(f"https://{os.environ[github_token]}:@github.com")
+ run('git config credential.helper "store --file=.git/credentials"')
+ push = "git push"
+ if push_quietly:
+ push += " -q"
+ if push_force:
+ push += " -f"
+ push += f" origin {github_branch}"
+ run(push, echo=True)
+ else:
+ print("No place to deploy.")
+
+
+def ablog_main():
+ """
+ Ablog Main.
+ """
+ if len(sys.argv) == 1:
+ parser.print_help()
+ else:
+ namespace = parser.parse_args()
+ namespace.func(**namespace.__dict__)
diff --git a/src/ablog/locales/ca/LC_MESSAGES/sphinx.mo b/src/ablog/locales/ca/LC_MESSAGES/sphinx.mo
new file mode 100644
index 0000000..3a9c5b7
--- /dev/null
+++ b/src/ablog/locales/ca/LC_MESSAGES/sphinx.mo
Binary files differ
diff --git a/src/ablog/locales/ca/LC_MESSAGES/sphinx.po b/src/ablog/locales/ca/LC_MESSAGES/sphinx.po
new file mode 100644
index 0000000..033837b
--- /dev/null
+++ b/src/ablog/locales/ca/LC_MESSAGES/sphinx.po
@@ -0,0 +1,129 @@
+# Catalan translations for ablog.
+# Copyright (C) 2021 ORGANIZATION
+# This file is distributed under the same license as the ablog project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2021.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: ablog 0.10.12\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2022-11-14 16:46-0800\n"
+"PO-Revision-Date: 2021-09-18 18:57+0200\n"
+"Last-Translator: Francesc Vilardell Sallés <vilardellsalles@gmail.com>\n"
+"Language: ca\n"
+"Language-Team: ca <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.11.0\n"
+
+#: ablog/post.py:272
+msgid "Updated on "
+msgstr "Actualitzat el "
+
+#: ablog/post.py:564 ablog/templates/ablog/authors.html:3
+#: ablog/templates/authors.html:4
+msgid "Authors"
+msgstr "Autors"
+
+#: ablog/post.py:564 ablog/post.py:631
+msgid "Posts by"
+msgstr "Publicacions de"
+
+#: ablog/post.py:565 ablog/templates/ablog/locations.html:4
+#: ablog/templates/locations.html:5
+msgid "Locations"
+msgstr "Ubicacions"
+
+#: ablog/post.py:565 ablog/post.py:632
+msgid "Posts from"
+msgstr "Publicacions des de"
+
+#: ablog/post.py:566 ablog/templates/ablog/languages.html:4
+#: ablog/templates/languages.html:5
+msgid "Languages"
+msgstr "Idiomes"
+
+#: ablog/post.py:566 ablog/post.py:567 ablog/post.py:633 ablog/post.py:634
+msgid "Posts in"
+msgstr "Publicacions en"
+
+#: ablog/post.py:567 ablog/templates/ablog/categories.html:4
+#: ablog/templates/categories.html:5
+msgid "Categories"
+msgstr "Categories"
+
+#: ablog/post.py:568
+msgid "All posts"
+msgstr "Totes les publicacions"
+
+#: ablog/post.py:568 ablog/post.py:635
+msgid "Posted in"
+msgstr "Publicat al"
+
+#: ablog/post.py:569 ablog/templates/ablog/postcard2.html:117
+#: ablog/templates/ablog/tagcloud.html:3 ablog/templates/postcard2.html:118
+#: ablog/templates/tagcloud.html:4
+msgid "Tags"
+msgstr "Etiquetes"
+
+#: ablog/post.py:569 ablog/post.py:636
+msgid "Posts tagged"
+msgstr "Publicacions amb etiqueta"
+
+#: ablog/post.py:594
+msgid "All Posts"
+msgstr "Totes les publicacions"
+
+#: ablog/post.py:595
+msgid "All"
+msgstr "Totes les"
+
+#: ablog/post.py:603
+msgid "Drafts"
+msgstr "Borradors"
+
+#: ablog/templates/ablog/archives.html:4 ablog/templates/archives.html:5
+msgid "Archives"
+msgstr "Arxius"
+
+#: ablog/templates/ablog/collection.html:56 ablog/templates/collection.html:57
+msgid "Read more ..."
+msgstr "Llegir-ne més ..."
+
+#: ablog/templates/ablog/postcard2.html:8 ablog/templates/postcard2.html:9
+msgid "Update"
+msgstr "Actualització"
+
+#: ablog/templates/ablog/postcard2.html:20 ablog/templates/postcard2.html:21
+msgid "Author"
+msgstr "Autor"
+
+#: ablog/templates/ablog/postcard2.html:44 ablog/templates/postcard2.html:45
+msgid "Location"
+msgstr "Ubicació"
+
+#: ablog/templates/ablog/postcard2.html:68 ablog/templates/postcard2.html:69
+msgid "Language"
+msgstr "Idioma"
+
+#: ablog/templates/ablog/postcard2.html:92 ablog/templates/postcard2.html:93
+msgid "Category"
+msgstr "Categoria"
+
+#: ablog/templates/ablog/postcard2.html:123 ablog/templates/postcard2.html:124
+msgid "Tag"
+msgstr "Etiqueta"
+
+#: ablog/templates/ablog/postnavy.html:8 ablog/templates/postnavy.html:9
+msgid "Previous"
+msgstr "Anterior"
+
+#: ablog/templates/ablog/postnavy.html:22 ablog/templates/postnavy.html:23
+msgid "Next"
+msgstr "Següent"
+
+#: ablog/templates/ablog/recentposts.html:4 ablog/templates/recentposts.html:5
+msgid "Recent Posts"
+msgstr "Publicacions recents"
diff --git a/src/ablog/locales/de/LC_MESSAGES/sphinx.mo b/src/ablog/locales/de/LC_MESSAGES/sphinx.mo
new file mode 100644
index 0000000..9f556bb
--- /dev/null
+++ b/src/ablog/locales/de/LC_MESSAGES/sphinx.mo
Binary files differ
diff --git a/src/ablog/locales/de/LC_MESSAGES/sphinx.po b/src/ablog/locales/de/LC_MESSAGES/sphinx.po
new file mode 100644
index 0000000..73cf332
--- /dev/null
+++ b/src/ablog/locales/de/LC_MESSAGES/sphinx.po
@@ -0,0 +1,131 @@
+# German translations for ablog.
+# Copyright (C) 2014 ORGANIZATION
+# This file is distributed under the same license as the ablog project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: ablog 0.1\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2022-11-14 16:46-0800\n"
+"PO-Revision-Date: 2014-07-25 20:46+0300\n"
+"Last-Translator: Luc Saffre <luc.saffre@gmx.net>\n"
+"Language: de\n"
+"Language-Team: de <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.11.0\n"
+
+#: ablog/post.py:272
+#, fuzzy
+msgid "Updated on "
+msgstr "Aktualisiert am"
+
+#: ablog/post.py:564 ablog/templates/ablog/authors.html:3
+#: ablog/templates/authors.html:4
+msgid "Authors"
+msgstr "Autoren"
+
+#: ablog/post.py:564 ablog/post.py:631
+msgid "Posts by"
+msgstr "Einträge von"
+
+#: ablog/post.py:565 ablog/templates/ablog/locations.html:4
+#: ablog/templates/locations.html:5
+msgid "Locations"
+msgstr "Orte"
+
+#: ablog/post.py:565 ablog/post.py:632
+msgid "Posts from"
+msgstr "Einträge aus"
+
+#: ablog/post.py:566 ablog/templates/ablog/languages.html:4
+#: ablog/templates/languages.html:5
+msgid "Languages"
+msgstr "Sprachen"
+
+#: ablog/post.py:566 ablog/post.py:567 ablog/post.py:633 ablog/post.py:634
+msgid "Posts in"
+msgstr "Einträge in"
+
+#: ablog/post.py:567 ablog/templates/ablog/categories.html:4
+#: ablog/templates/categories.html:5
+msgid "Categories"
+msgstr "Kategorien"
+
+#: ablog/post.py:568
+msgid "All posts"
+msgstr "Alle Einträge"
+
+#: ablog/post.py:568 ablog/post.py:635
+msgid "Posted in"
+msgstr "Einträge in"
+
+#: ablog/post.py:569 ablog/templates/ablog/postcard2.html:117
+#: ablog/templates/ablog/tagcloud.html:3 ablog/templates/postcard2.html:118
+#: ablog/templates/tagcloud.html:4
+msgid "Tags"
+msgstr "Schlagworte"
+
+#: ablog/post.py:569 ablog/post.py:636
+msgid "Posts tagged"
+msgstr "Einträge mit Schlagwort"
+
+#: ablog/post.py:594
+#, fuzzy
+msgid "All Posts"
+msgstr "Alle Einträge"
+
+#: ablog/post.py:595
+msgid "All"
+msgstr ""
+
+#: ablog/post.py:603
+msgid "Drafts"
+msgstr "Entwurf"
+
+#: ablog/templates/ablog/archives.html:4 ablog/templates/archives.html:5
+msgid "Archives"
+msgstr "Archive"
+
+#: ablog/templates/ablog/collection.html:56 ablog/templates/collection.html:57
+msgid "Read more ..."
+msgstr "Weiter..."
+
+#: ablog/templates/ablog/postcard2.html:8 ablog/templates/postcard2.html:9
+msgid "Update"
+msgstr "Aktualisierung"
+
+#: ablog/templates/ablog/postcard2.html:20 ablog/templates/postcard2.html:21
+msgid "Author"
+msgstr "Autor"
+
+#: ablog/templates/ablog/postcard2.html:44 ablog/templates/postcard2.html:45
+msgid "Location"
+msgstr "Ort"
+
+#: ablog/templates/ablog/postcard2.html:68 ablog/templates/postcard2.html:69
+msgid "Language"
+msgstr "Sprache"
+
+#: ablog/templates/ablog/postcard2.html:92 ablog/templates/postcard2.html:93
+msgid "Category"
+msgstr "Kategorie"
+
+#: ablog/templates/ablog/postcard2.html:123 ablog/templates/postcard2.html:124
+msgid "Tag"
+msgstr "Schlagwort"
+
+#: ablog/templates/ablog/postnavy.html:8 ablog/templates/postnavy.html:9
+msgid "Previous"
+msgstr "Vorige"
+
+#: ablog/templates/ablog/postnavy.html:22 ablog/templates/postnavy.html:23
+msgid "Next"
+msgstr "Nächste"
+
+#: ablog/templates/ablog/recentposts.html:4 ablog/templates/recentposts.html:5
+msgid "Recent Posts"
+msgstr "Neue Einträge"
diff --git a/src/ablog/locales/es/LC_MESSAGES/sphinx.mo b/src/ablog/locales/es/LC_MESSAGES/sphinx.mo
new file mode 100644
index 0000000..dfc892d
--- /dev/null
+++ b/src/ablog/locales/es/LC_MESSAGES/sphinx.mo
Binary files differ
diff --git a/src/ablog/locales/es/LC_MESSAGES/sphinx.po b/src/ablog/locales/es/LC_MESSAGES/sphinx.po
new file mode 100644
index 0000000..ca06242
--- /dev/null
+++ b/src/ablog/locales/es/LC_MESSAGES/sphinx.po
@@ -0,0 +1,131 @@
+# Spanish translations for ablog.
+# Copyright (C) 2014 ORGANIZATION
+# This file is distributed under the same license as the ablog project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: ablog 0.2.3\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2022-11-14 16:46-0800\n"
+"PO-Revision-Date: 2016-04-21 14:26+0200\n"
+"Last-Translator: José Carlos García <jcg@quobit.net>\n"
+"Language: es\n"
+"Language-Team: es <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.11.0\n"
+
+#: ablog/post.py:272
+#, fuzzy
+msgid "Updated on "
+msgstr "Actualizado el"
+
+#: ablog/post.py:564 ablog/templates/ablog/authors.html:3
+#: ablog/templates/authors.html:4
+msgid "Authors"
+msgstr "Autores"
+
+#: ablog/post.py:564 ablog/post.py:631
+msgid "Posts by"
+msgstr "Entradas por"
+
+#: ablog/post.py:565 ablog/templates/ablog/locations.html:4
+#: ablog/templates/locations.html:5
+msgid "Locations"
+msgstr "Lugares"
+
+#: ablog/post.py:565 ablog/post.py:632
+msgid "Posts from"
+msgstr "Entradas desde"
+
+#: ablog/post.py:566 ablog/templates/ablog/languages.html:4
+#: ablog/templates/languages.html:5
+msgid "Languages"
+msgstr "Idiomas"
+
+#: ablog/post.py:566 ablog/post.py:567 ablog/post.py:633 ablog/post.py:634
+msgid "Posts in"
+msgstr "Entradas en"
+
+#: ablog/post.py:567 ablog/templates/ablog/categories.html:4
+#: ablog/templates/categories.html:5
+msgid "Categories"
+msgstr "Categorías"
+
+#: ablog/post.py:568
+msgid "All posts"
+msgstr "Todas las entradas"
+
+#: ablog/post.py:568 ablog/post.py:635
+msgid "Posted in"
+msgstr "Publicado en"
+
+#: ablog/post.py:569 ablog/templates/ablog/postcard2.html:117
+#: ablog/templates/ablog/tagcloud.html:3 ablog/templates/postcard2.html:118
+#: ablog/templates/tagcloud.html:4
+msgid "Tags"
+msgstr "Etiquetas"
+
+#: ablog/post.py:569 ablog/post.py:636
+msgid "Posts tagged"
+msgstr "Entradas etiquetadas"
+
+#: ablog/post.py:594
+#, fuzzy
+msgid "All Posts"
+msgstr "Todas las entradas"
+
+#: ablog/post.py:595
+msgid "All"
+msgstr ""
+
+#: ablog/post.py:603
+msgid "Drafts"
+msgstr "Borradores"
+
+#: ablog/templates/ablog/archives.html:4 ablog/templates/archives.html:5
+msgid "Archives"
+msgstr "Archivos"
+
+#: ablog/templates/ablog/collection.html:56 ablog/templates/collection.html:57
+msgid "Read more ..."
+msgstr "Leer más ..."
+
+#: ablog/templates/ablog/postcard2.html:8 ablog/templates/postcard2.html:9
+msgid "Update"
+msgstr "Actualizado"
+
+#: ablog/templates/ablog/postcard2.html:20 ablog/templates/postcard2.html:21
+msgid "Author"
+msgstr "Autor"
+
+#: ablog/templates/ablog/postcard2.html:44 ablog/templates/postcard2.html:45
+msgid "Location"
+msgstr "Lugar"
+
+#: ablog/templates/ablog/postcard2.html:68 ablog/templates/postcard2.html:69
+msgid "Language"
+msgstr "Idioma"
+
+#: ablog/templates/ablog/postcard2.html:92 ablog/templates/postcard2.html:93
+msgid "Category"
+msgstr "Categoría"
+
+#: ablog/templates/ablog/postcard2.html:123 ablog/templates/postcard2.html:124
+msgid "Tag"
+msgstr "Etiqueta"
+
+#: ablog/templates/ablog/postnavy.html:8 ablog/templates/postnavy.html:9
+msgid "Previous"
+msgstr "Anterior"
+
+#: ablog/templates/ablog/postnavy.html:22 ablog/templates/postnavy.html:23
+msgid "Next"
+msgstr "Siguiente"
+
+#: ablog/templates/ablog/recentposts.html:4 ablog/templates/recentposts.html:5
+msgid "Recent Posts"
+msgstr "Entradas recientes"
diff --git a/src/ablog/locales/et/LC_MESSAGES/sphinx.mo b/src/ablog/locales/et/LC_MESSAGES/sphinx.mo
new file mode 100644
index 0000000..85b1df9
--- /dev/null
+++ b/src/ablog/locales/et/LC_MESSAGES/sphinx.mo
Binary files differ
diff --git a/src/ablog/locales/et/LC_MESSAGES/sphinx.po b/src/ablog/locales/et/LC_MESSAGES/sphinx.po
new file mode 100644
index 0000000..18d526f
--- /dev/null
+++ b/src/ablog/locales/et/LC_MESSAGES/sphinx.po
@@ -0,0 +1,131 @@
+# Estonian translations for ablog.
+# Copyright (C) 2014 ORGANIZATION
+# This file is distributed under the same license as the ablog project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: ablog 0.1\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2022-11-14 16:46-0800\n"
+"PO-Revision-Date: 2014-07-31 09:13+0300\n"
+"Last-Translator: Luc Saffre <luc.saffre@gmx.net>\n"
+"Language: et\n"
+"Language-Team: et <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.11.0\n"
+
+#: ablog/post.py:272
+#, fuzzy
+msgid "Updated on "
+msgstr "Uuendus"
+
+#: ablog/post.py:564 ablog/templates/ablog/authors.html:3
+#: ablog/templates/authors.html:4
+msgid "Authors"
+msgstr "Autorid"
+
+#: ablog/post.py:564 ablog/post.py:631
+msgid "Posts by"
+msgstr "Postitused autorilt"
+
+#: ablog/post.py:565 ablog/templates/ablog/locations.html:4
+#: ablog/templates/locations.html:5
+msgid "Locations"
+msgstr "Kohad"
+
+#: ablog/post.py:565 ablog/post.py:632
+msgid "Posts from"
+msgstr ""
+
+#: ablog/post.py:566 ablog/templates/ablog/languages.html:4
+#: ablog/templates/languages.html:5
+msgid "Languages"
+msgstr "Keeltes"
+
+#: ablog/post.py:566 ablog/post.py:567 ablog/post.py:633 ablog/post.py:634
+msgid "Posts in"
+msgstr ""
+
+#: ablog/post.py:567 ablog/templates/ablog/categories.html:4
+#: ablog/templates/categories.html:5
+msgid "Categories"
+msgstr "Kategooriad"
+
+#: ablog/post.py:568
+msgid "All posts"
+msgstr "Kõik postitused"
+
+#: ablog/post.py:568 ablog/post.py:635
+msgid "Posted in"
+msgstr "Postitused kategoorias"
+
+#: ablog/post.py:569 ablog/templates/ablog/postcard2.html:117
+#: ablog/templates/ablog/tagcloud.html:3 ablog/templates/postcard2.html:118
+#: ablog/templates/tagcloud.html:4
+msgid "Tags"
+msgstr "Märksõnad"
+
+#: ablog/post.py:569 ablog/post.py:636
+msgid "Posts tagged"
+msgstr "Postitused märksõnaga"
+
+#: ablog/post.py:594
+#, fuzzy
+msgid "All Posts"
+msgstr "Kõik postitused"
+
+#: ablog/post.py:595
+msgid "All"
+msgstr ""
+
+#: ablog/post.py:603
+msgid "Drafts"
+msgstr "Eelnõu"
+
+#: ablog/templates/ablog/archives.html:4 ablog/templates/archives.html:5
+msgid "Archives"
+msgstr "Arhiiv"
+
+#: ablog/templates/ablog/collection.html:56 ablog/templates/collection.html:57
+msgid "Read more ..."
+msgstr "Edasi..."
+
+#: ablog/templates/ablog/postcard2.html:8 ablog/templates/postcard2.html:9
+msgid "Update"
+msgstr "Ajakohastama"
+
+#: ablog/templates/ablog/postcard2.html:20 ablog/templates/postcard2.html:21
+msgid "Author"
+msgstr "Autor"
+
+#: ablog/templates/ablog/postcard2.html:44 ablog/templates/postcard2.html:45
+msgid "Location"
+msgstr "Koht"
+
+#: ablog/templates/ablog/postcard2.html:68 ablog/templates/postcard2.html:69
+msgid "Language"
+msgstr "Keel"
+
+#: ablog/templates/ablog/postcard2.html:92 ablog/templates/postcard2.html:93
+msgid "Category"
+msgstr "Kategooria"
+
+#: ablog/templates/ablog/postcard2.html:123 ablog/templates/postcard2.html:124
+msgid "Tag"
+msgstr "Märksõna"
+
+#: ablog/templates/ablog/postnavy.html:8 ablog/templates/postnavy.html:9
+msgid "Previous"
+msgstr "Eelmine"
+
+#: ablog/templates/ablog/postnavy.html:22 ablog/templates/postnavy.html:23
+msgid "Next"
+msgstr "Järgmine"
+
+#: ablog/templates/ablog/recentposts.html:4 ablog/templates/recentposts.html:5
+msgid "Recent Posts"
+msgstr "Viimased postitused"
diff --git a/src/ablog/locales/fr/LC_MESSAGES/sphinx.mo b/src/ablog/locales/fr/LC_MESSAGES/sphinx.mo
new file mode 100644
index 0000000..6711719
--- /dev/null
+++ b/src/ablog/locales/fr/LC_MESSAGES/sphinx.mo
Binary files differ
diff --git a/src/ablog/locales/fr/LC_MESSAGES/sphinx.po b/src/ablog/locales/fr/LC_MESSAGES/sphinx.po
new file mode 100644
index 0000000..810f35a
--- /dev/null
+++ b/src/ablog/locales/fr/LC_MESSAGES/sphinx.po
@@ -0,0 +1,130 @@
+# French translations for ablog.
+# Copyright (C) 2020 ORGANIZATION
+# This file is distributed under the same license as the ablog project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: ablog 0.10.5\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2022-11-14 16:46-0800\n"
+"PO-Revision-Date: 2020-05-22 20:04+0200\n"
+"Last-Translator: \n"
+"Language: fr\n"
+"Language-Team: fr <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.11.0\n"
+
+#: ablog/post.py:272
+#, fuzzy
+msgid "Updated on "
+msgstr "Mis à jour"
+
+#: ablog/post.py:564 ablog/templates/ablog/authors.html:3
+#: ablog/templates/authors.html:4
+msgid "Authors"
+msgstr "Auteurs"
+
+#: ablog/post.py:564 ablog/post.py:631
+msgid "Posts by"
+msgstr "Billets par"
+
+#: ablog/post.py:565 ablog/templates/ablog/locations.html:4
+#: ablog/templates/locations.html:5
+msgid "Locations"
+msgstr "Lieux"
+
+#: ablog/post.py:565 ablog/post.py:632
+msgid "Posts from"
+msgstr "Billets depuis"
+
+#: ablog/post.py:566 ablog/templates/ablog/languages.html:4
+#: ablog/templates/languages.html:5
+msgid "Languages"
+msgstr "Langues"
+
+#: ablog/post.py:566 ablog/post.py:567 ablog/post.py:633 ablog/post.py:634
+msgid "Posts in"
+msgstr "Billets dans"
+
+#: ablog/post.py:567 ablog/templates/ablog/categories.html:4
+#: ablog/templates/categories.html:5
+msgid "Categories"
+msgstr "Catégories"
+
+#: ablog/post.py:568
+msgid "All posts"
+msgstr "Tous les billets"
+
+#: ablog/post.py:568 ablog/post.py:635
+msgid "Posted in"
+msgstr "Publié dans"
+
+#: ablog/post.py:569 ablog/templates/ablog/postcard2.html:117
+#: ablog/templates/ablog/tagcloud.html:3 ablog/templates/postcard2.html:118
+#: ablog/templates/tagcloud.html:4
+msgid "Tags"
+msgstr "Tags"
+
+#: ablog/post.py:569 ablog/post.py:636
+msgid "Posts tagged"
+msgstr "Billets tagués"
+
+#: ablog/post.py:594
+msgid "All Posts"
+msgstr "Tous les billets"
+
+#: ablog/post.py:595
+msgid "All"
+msgstr "Tous les billets"
+
+#: ablog/post.py:603
+msgid "Drafts"
+msgstr "Brouillons"
+
+#: ablog/templates/ablog/archives.html:4 ablog/templates/archives.html:5
+msgid "Archives"
+msgstr "Archives"
+
+#: ablog/templates/ablog/collection.html:56 ablog/templates/collection.html:57
+msgid "Read more ..."
+msgstr "Lire plus…"
+
+#: ablog/templates/ablog/postcard2.html:8 ablog/templates/postcard2.html:9
+msgid "Update"
+msgstr "Mis à jour"
+
+#: ablog/templates/ablog/postcard2.html:20 ablog/templates/postcard2.html:21
+msgid "Author"
+msgstr "Auteur"
+
+#: ablog/templates/ablog/postcard2.html:44 ablog/templates/postcard2.html:45
+msgid "Location"
+msgstr "Lieu"
+
+#: ablog/templates/ablog/postcard2.html:68 ablog/templates/postcard2.html:69
+msgid "Language"
+msgstr "Langue"
+
+#: ablog/templates/ablog/postcard2.html:92 ablog/templates/postcard2.html:93
+msgid "Category"
+msgstr "Catégorie"
+
+#: ablog/templates/ablog/postcard2.html:123 ablog/templates/postcard2.html:124
+msgid "Tag"
+msgstr "Tag"
+
+#: ablog/templates/ablog/postnavy.html:8 ablog/templates/postnavy.html:9
+msgid "Previous"
+msgstr "Précédent"
+
+#: ablog/templates/ablog/postnavy.html:22 ablog/templates/postnavy.html:23
+msgid "Next"
+msgstr "Suivant"
+
+#: ablog/templates/ablog/recentposts.html:4 ablog/templates/recentposts.html:5
+msgid "Recent Posts"
+msgstr "Billets récents"
diff --git a/src/ablog/locales/it/LC_MESSAGES/sphinx.mo b/src/ablog/locales/it/LC_MESSAGES/sphinx.mo
new file mode 100644
index 0000000..2c2495d
--- /dev/null
+++ b/src/ablog/locales/it/LC_MESSAGES/sphinx.mo
Binary files differ
diff --git a/src/ablog/locales/it/LC_MESSAGES/sphinx.po b/src/ablog/locales/it/LC_MESSAGES/sphinx.po
new file mode 100644
index 0000000..8c97760
--- /dev/null
+++ b/src/ablog/locales/it/LC_MESSAGES/sphinx.po
@@ -0,0 +1,130 @@
+# Translations template for ablog.
+# Copyright (C) 2022 ORGANIZATION
+# This file is distributed under the same license as the ablog project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: ablog 0.10.30.dev19+gb9b1a31\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2022-11-14 16:46-0800\n"
+"PO-Revision-Date: 2023-09-15 11:32+0200\n"
+"Last-Translator: Stefano David <code@stefanodavid.eu>\n"
+"Language-Team: \n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Generated-By: Babel 2.11.0\n"
+"X-Generator: Poedit 3.3.2\n"
+
+#: ablog/post.py:272
+msgid "Updated on "
+msgstr "Aggiornato il "
+
+#: ablog/post.py:564 ablog/templates/ablog/authors.html:3
+#: ablog/templates/authors.html:4
+msgid "Authors"
+msgstr "Autori"
+
+#: ablog/post.py:564 ablog/post.py:631
+msgid "Posts by"
+msgstr "Articoli di"
+
+#: ablog/post.py:565 ablog/templates/ablog/locations.html:4
+#: ablog/templates/locations.html:5
+msgid "Locations"
+msgstr "Località"
+
+#: ablog/post.py:565 ablog/post.py:632
+msgid "Posts from"
+msgstr "Articolo da"
+
+#: ablog/post.py:566 ablog/templates/ablog/languages.html:4
+#: ablog/templates/languages.html:5
+msgid "Languages"
+msgstr "Lingue"
+
+#: ablog/post.py:566 ablog/post.py:567 ablog/post.py:633 ablog/post.py:634
+msgid "Posts in"
+msgstr "Articoli in"
+
+#: ablog/post.py:567 ablog/templates/ablog/categories.html:4
+#: ablog/templates/categories.html:5
+msgid "Categories"
+msgstr "Categorie"
+
+#: ablog/post.py:568
+msgid "All posts"
+msgstr "Tutti gli articoli"
+
+#: ablog/post.py:568 ablog/post.py:635
+msgid "Posted in"
+msgstr "Pubblicato in"
+
+#: ablog/post.py:569 ablog/templates/ablog/postcard2.html:117
+#: ablog/templates/ablog/tagcloud.html:3 ablog/templates/postcard2.html:118
+#: ablog/templates/tagcloud.html:4
+msgid "Tags"
+msgstr "Tag"
+
+#: ablog/post.py:569 ablog/post.py:636
+msgid "Posts tagged"
+msgstr "Articoli con tag"
+
+#: ablog/post.py:594
+msgid "All Posts"
+msgstr "Tutti gli articoli"
+
+#: ablog/post.py:595
+msgid "All"
+msgstr "Tutti"
+
+#: ablog/post.py:603
+msgid "Drafts"
+msgstr "Bozze"
+
+#: ablog/templates/ablog/archives.html:4 ablog/templates/archives.html:5
+msgid "Archives"
+msgstr "Archivi"
+
+#: ablog/templates/ablog/collection.html:56 ablog/templates/collection.html:57
+msgid "Read more ..."
+msgstr "Leggi di più..."
+
+#: ablog/templates/ablog/postcard2.html:8 ablog/templates/postcard2.html:9
+msgid "Update"
+msgstr "Aggiornamento"
+
+#: ablog/templates/ablog/postcard2.html:20 ablog/templates/postcard2.html:21
+msgid "Author"
+msgstr "Autore"
+
+#: ablog/templates/ablog/postcard2.html:44 ablog/templates/postcard2.html:45
+msgid "Location"
+msgstr "Località"
+
+#: ablog/templates/ablog/postcard2.html:68 ablog/templates/postcard2.html:69
+msgid "Language"
+msgstr "Lingua"
+
+#: ablog/templates/ablog/postcard2.html:92 ablog/templates/postcard2.html:93
+msgid "Category"
+msgstr "Categoria"
+
+#: ablog/templates/ablog/postcard2.html:123 ablog/templates/postcard2.html:124
+msgid "Tag"
+msgstr "Tag"
+
+#: ablog/templates/ablog/postnavy.html:8 ablog/templates/postnavy.html:9
+msgid "Previous"
+msgstr "Precedente"
+
+#: ablog/templates/ablog/postnavy.html:22 ablog/templates/postnavy.html:23
+msgid "Next"
+msgstr "Successivo"
+
+#: ablog/templates/ablog/recentposts.html:4 ablog/templates/recentposts.html:5
+msgid "Recent Posts"
+msgstr "Articoli recenti"
diff --git a/src/ablog/locales/pt/LC_MESSAGES/sphinx.mo b/src/ablog/locales/pt/LC_MESSAGES/sphinx.mo
new file mode 100644
index 0000000..b94ff4f
--- /dev/null
+++ b/src/ablog/locales/pt/LC_MESSAGES/sphinx.mo
Binary files differ
diff --git a/src/ablog/locales/pt/LC_MESSAGES/sphinx.po b/src/ablog/locales/pt/LC_MESSAGES/sphinx.po
new file mode 100644
index 0000000..e5ca2f2
--- /dev/null
+++ b/src/ablog/locales/pt/LC_MESSAGES/sphinx.po
@@ -0,0 +1,129 @@
+# Portuguese translations for ablog.
+# Copyright (C) 2022 ORGANIZATION
+# This file is distributed under the same license as the ablog project.
+# Luís Henriques <henrix@camandro.org>, 2022.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: ablog 0.10.30.dev7+g9a43710e3a7d\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2022-11-14 16:46-0800\n"
+"PO-Revision-Date: 2022-10-02 20:41+0100\n"
+"Last-Translator: Luís Henriques <henrix@camandro.org>\n"
+"Language: pt\n"
+"Language-Team: pt <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.11.0\n"
+
+#: ablog/post.py:272
+msgid "Updated on "
+msgstr "Actualizado em "
+
+#: ablog/post.py:564 ablog/templates/ablog/authors.html:3
+#: ablog/templates/authors.html:4
+msgid "Authors"
+msgstr "Autores"
+
+#: ablog/post.py:564 ablog/post.py:631
+msgid "Posts by"
+msgstr "Entradas por"
+
+#: ablog/post.py:565 ablog/templates/ablog/locations.html:4
+#: ablog/templates/locations.html:5
+msgid "Locations"
+msgstr "Localizações"
+
+#: ablog/post.py:565 ablog/post.py:632
+msgid "Posts from"
+msgstr "Entradas de"
+
+#: ablog/post.py:566 ablog/templates/ablog/languages.html:4
+#: ablog/templates/languages.html:5
+msgid "Languages"
+msgstr "Linguagens"
+
+#: ablog/post.py:566 ablog/post.py:567 ablog/post.py:633 ablog/post.py:634
+msgid "Posts in"
+msgstr "Entradas em"
+
+#: ablog/post.py:567 ablog/templates/ablog/categories.html:4
+#: ablog/templates/categories.html:5
+msgid "Categories"
+msgstr "Categorias"
+
+#: ablog/post.py:568
+msgid "All posts"
+msgstr "Todas as entradas"
+
+#: ablog/post.py:568 ablog/post.py:635
+msgid "Posted in"
+msgstr "Publicado em"
+
+#: ablog/post.py:569 ablog/templates/ablog/postcard2.html:117
+#: ablog/templates/ablog/tagcloud.html:3 ablog/templates/postcard2.html:118
+#: ablog/templates/tagcloud.html:4
+msgid "Tags"
+msgstr "Etiquetas"
+
+#: ablog/post.py:569 ablog/post.py:636
+msgid "Posts tagged"
+msgstr "Entradas com etiqueta"
+
+#: ablog/post.py:594
+msgid "All Posts"
+msgstr "Todas as Entradas"
+
+#: ablog/post.py:595
+msgid "All"
+msgstr "Todas"
+
+#: ablog/post.py:603
+msgid "Drafts"
+msgstr "Rascunhos"
+
+#: ablog/templates/ablog/archives.html:4 ablog/templates/archives.html:5
+msgid "Archives"
+msgstr "Arquivos"
+
+#: ablog/templates/ablog/collection.html:56 ablog/templates/collection.html:57
+msgid "Read more ..."
+msgstr "Ler mais ..."
+
+#: ablog/templates/ablog/postcard2.html:8 ablog/templates/postcard2.html:9
+msgid "Update"
+msgstr "Actualizado"
+
+#: ablog/templates/ablog/postcard2.html:20 ablog/templates/postcard2.html:21
+msgid "Author"
+msgstr "Autor"
+
+#: ablog/templates/ablog/postcard2.html:44 ablog/templates/postcard2.html:45
+msgid "Location"
+msgstr "Localização"
+
+#: ablog/templates/ablog/postcard2.html:68 ablog/templates/postcard2.html:69
+msgid "Language"
+msgstr "Idioma"
+
+#: ablog/templates/ablog/postcard2.html:92 ablog/templates/postcard2.html:93
+msgid "Category"
+msgstr "Categoria"
+
+#: ablog/templates/ablog/postcard2.html:123 ablog/templates/postcard2.html:124
+msgid "Tag"
+msgstr "Etiqueta"
+
+#: ablog/templates/ablog/postnavy.html:8 ablog/templates/postnavy.html:9
+msgid "Previous"
+msgstr "Anterior"
+
+#: ablog/templates/ablog/postnavy.html:22 ablog/templates/postnavy.html:23
+msgid "Next"
+msgstr "Próximo"
+
+#: ablog/templates/ablog/recentposts.html:4 ablog/templates/recentposts.html:5
+msgid "Recent Posts"
+msgstr "Entradas Recentes"
diff --git a/src/ablog/locales/ru/LC_MESSAGES/sphinx.mo b/src/ablog/locales/ru/LC_MESSAGES/sphinx.mo
new file mode 100644
index 0000000..8558aec
--- /dev/null
+++ b/src/ablog/locales/ru/LC_MESSAGES/sphinx.mo
Binary files differ
diff --git a/src/ablog/locales/ru/LC_MESSAGES/sphinx.po b/src/ablog/locales/ru/LC_MESSAGES/sphinx.po
new file mode 100644
index 0000000..c32d4a5
--- /dev/null
+++ b/src/ablog/locales/ru/LC_MESSAGES/sphinx.po
@@ -0,0 +1,132 @@
+# Russian translations for ablog.
+# Copyright (C) 2014 ORGANIZATION
+# This file is distributed under the same license as the ablog project.
+# uralbash <root@uralbash.ru>, 2014.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: ablog 0.2.3\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2022-11-14 16:46-0800\n"
+"PO-Revision-Date: 2015-04-14 17:10+0500\n"
+"Last-Translator: uralbash <root@uralbash.ru>\n"
+"Language: ru\n"
+"Language-Team: Russian\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.11.0\n"
+
+#: ablog/post.py:272
+#, fuzzy
+msgid "Updated on "
+msgstr "Обновлено"
+
+#: ablog/post.py:564 ablog/templates/ablog/authors.html:3
+#: ablog/templates/authors.html:4
+msgid "Authors"
+msgstr "Авторы"
+
+#: ablog/post.py:564 ablog/post.py:631
+msgid "Posts by"
+msgstr "Опубликовано"
+
+#: ablog/post.py:565 ablog/templates/ablog/locations.html:4
+#: ablog/templates/locations.html:5
+msgid "Locations"
+msgstr ""
+
+#: ablog/post.py:565 ablog/post.py:632
+msgid "Posts from"
+msgstr "Сообщения из"
+
+#: ablog/post.py:566 ablog/templates/ablog/languages.html:4
+#: ablog/templates/languages.html:5
+msgid "Languages"
+msgstr "Языки"
+
+#: ablog/post.py:566 ablog/post.py:567 ablog/post.py:633 ablog/post.py:634
+msgid "Posts in"
+msgstr "Сообщений в"
+
+#: ablog/post.py:567 ablog/templates/ablog/categories.html:4
+#: ablog/templates/categories.html:5
+msgid "Categories"
+msgstr "Категории"
+
+#: ablog/post.py:568
+msgid "All posts"
+msgstr "Все записи"
+
+#: ablog/post.py:568 ablog/post.py:635
+msgid "Posted in"
+msgstr "Опубликовано в"
+
+#: ablog/post.py:569 ablog/templates/ablog/postcard2.html:117
+#: ablog/templates/ablog/tagcloud.html:3 ablog/templates/postcard2.html:118
+#: ablog/templates/tagcloud.html:4
+msgid "Tags"
+msgstr "Теги"
+
+#: ablog/post.py:569 ablog/post.py:636
+msgid "Posts tagged"
+msgstr "Сообщения с тегом"
+
+#: ablog/post.py:594
+#, fuzzy
+msgid "All Posts"
+msgstr "Все записи"
+
+#: ablog/post.py:595
+msgid "All"
+msgstr ""
+
+#: ablog/post.py:603
+msgid "Drafts"
+msgstr "Черновик"
+
+#: ablog/templates/ablog/archives.html:4 ablog/templates/archives.html:5
+msgid "Archives"
+msgstr "Архив"
+
+#: ablog/templates/ablog/collection.html:56 ablog/templates/collection.html:57
+msgid "Read more ..."
+msgstr "Читать ..."
+
+#: ablog/templates/ablog/postcard2.html:8 ablog/templates/postcard2.html:9
+msgid "Update"
+msgstr "Обновить"
+
+#: ablog/templates/ablog/postcard2.html:20 ablog/templates/postcard2.html:21
+msgid "Author"
+msgstr "Автор"
+
+#: ablog/templates/ablog/postcard2.html:44 ablog/templates/postcard2.html:45
+msgid "Location"
+msgstr "Расположение"
+
+#: ablog/templates/ablog/postcard2.html:68 ablog/templates/postcard2.html:69
+msgid "Language"
+msgstr "Язык"
+
+#: ablog/templates/ablog/postcard2.html:92 ablog/templates/postcard2.html:93
+msgid "Category"
+msgstr "Категория"
+
+#: ablog/templates/ablog/postcard2.html:123 ablog/templates/postcard2.html:124
+msgid "Tag"
+msgstr "Тег"
+
+#: ablog/templates/ablog/postnavy.html:8 ablog/templates/postnavy.html:9
+msgid "Previous"
+msgstr "Предыдущий"
+
+#: ablog/templates/ablog/postnavy.html:22 ablog/templates/postnavy.html:23
+msgid "Next"
+msgstr "Следующий"
+
+#: ablog/templates/ablog/recentposts.html:4 ablog/templates/recentposts.html:5
+msgid "Recent Posts"
+msgstr "Недавние Записи"
diff --git a/src/ablog/locales/sphinx.pot b/src/ablog/locales/sphinx.pot
new file mode 100644
index 0000000..e0be005
--- /dev/null
+++ b/src/ablog/locales/sphinx.pot
@@ -0,0 +1,128 @@
+# Translations template for ablog.
+# Copyright (C) 2022 ORGANIZATION
+# This file is distributed under the same license as the ablog project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: ablog 0.10.30.dev19+gb9b1a31\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2022-11-14 16:46-0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.11.0\n"
+
+#: ablog/post.py:272
+msgid "Updated on "
+msgstr ""
+
+#: ablog/post.py:564 ablog/templates/ablog/authors.html:3
+#: ablog/templates/authors.html:4
+msgid "Authors"
+msgstr ""
+
+#: ablog/post.py:564 ablog/post.py:631
+msgid "Posts by"
+msgstr ""
+
+#: ablog/post.py:565 ablog/templates/ablog/locations.html:4
+#: ablog/templates/locations.html:5
+msgid "Locations"
+msgstr ""
+
+#: ablog/post.py:565 ablog/post.py:632
+msgid "Posts from"
+msgstr ""
+
+#: ablog/post.py:566 ablog/templates/ablog/languages.html:4
+#: ablog/templates/languages.html:5
+msgid "Languages"
+msgstr ""
+
+#: ablog/post.py:566 ablog/post.py:567 ablog/post.py:633 ablog/post.py:634
+msgid "Posts in"
+msgstr ""
+
+#: ablog/post.py:567 ablog/templates/ablog/categories.html:4
+#: ablog/templates/categories.html:5
+msgid "Categories"
+msgstr ""
+
+#: ablog/post.py:568
+msgid "All posts"
+msgstr ""
+
+#: ablog/post.py:568 ablog/post.py:635
+msgid "Posted in"
+msgstr ""
+
+#: ablog/post.py:569 ablog/templates/ablog/postcard2.html:117
+#: ablog/templates/ablog/tagcloud.html:3 ablog/templates/postcard2.html:118
+#: ablog/templates/tagcloud.html:4
+msgid "Tags"
+msgstr ""
+
+#: ablog/post.py:569 ablog/post.py:636
+msgid "Posts tagged"
+msgstr ""
+
+#: ablog/post.py:594
+msgid "All Posts"
+msgstr ""
+
+#: ablog/post.py:595
+msgid "All"
+msgstr ""
+
+#: ablog/post.py:603
+msgid "Drafts"
+msgstr ""
+
+#: ablog/templates/ablog/archives.html:4 ablog/templates/archives.html:5
+msgid "Archives"
+msgstr ""
+
+#: ablog/templates/ablog/collection.html:56 ablog/templates/collection.html:57
+msgid "Read more ..."
+msgstr ""
+
+#: ablog/templates/ablog/postcard2.html:8 ablog/templates/postcard2.html:9
+msgid "Update"
+msgstr ""
+
+#: ablog/templates/ablog/postcard2.html:20 ablog/templates/postcard2.html:21
+msgid "Author"
+msgstr ""
+
+#: ablog/templates/ablog/postcard2.html:44 ablog/templates/postcard2.html:45
+msgid "Location"
+msgstr ""
+
+#: ablog/templates/ablog/postcard2.html:68 ablog/templates/postcard2.html:69
+msgid "Language"
+msgstr ""
+
+#: ablog/templates/ablog/postcard2.html:92 ablog/templates/postcard2.html:93
+msgid "Category"
+msgstr ""
+
+#: ablog/templates/ablog/postcard2.html:123 ablog/templates/postcard2.html:124
+msgid "Tag"
+msgstr ""
+
+#: ablog/templates/ablog/postnavy.html:8 ablog/templates/postnavy.html:9
+msgid "Previous"
+msgstr ""
+
+#: ablog/templates/ablog/postnavy.html:22 ablog/templates/postnavy.html:23
+msgid "Next"
+msgstr ""
+
+#: ablog/templates/ablog/recentposts.html:4 ablog/templates/recentposts.html:5
+msgid "Recent Posts"
+msgstr ""
diff --git a/src/ablog/locales/tr/LC_MESSAGES/sphinx.mo b/src/ablog/locales/tr/LC_MESSAGES/sphinx.mo
new file mode 100644
index 0000000..08f9e3f
--- /dev/null
+++ b/src/ablog/locales/tr/LC_MESSAGES/sphinx.mo
Binary files differ
diff --git a/src/ablog/locales/tr/LC_MESSAGES/sphinx.po b/src/ablog/locales/tr/LC_MESSAGES/sphinx.po
new file mode 100644
index 0000000..f1ac47b
--- /dev/null
+++ b/src/ablog/locales/tr/LC_MESSAGES/sphinx.po
@@ -0,0 +1,131 @@
+# Turkish translations for ablog.
+# Copyright (C) 2014 ORGANIZATION
+# This file is distributed under the same license as the ablog project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: ablog 0.1\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2022-11-14 16:46-0800\n"
+"PO-Revision-Date: 2014-08-01 21:43-0700\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: tr\n"
+"Language-Team: tr <LL@li.org>\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.11.0\n"
+
+#: ablog/post.py:272
+#, fuzzy
+msgid "Updated on "
+msgstr "Güncelleme"
+
+#: ablog/post.py:564 ablog/templates/ablog/authors.html:3
+#: ablog/templates/authors.html:4
+msgid "Authors"
+msgstr "Yazarlar"
+
+#: ablog/post.py:564 ablog/post.py:631
+msgid "Posts by"
+msgstr "Yazar"
+
+#: ablog/post.py:565 ablog/templates/ablog/locations.html:4
+#: ablog/templates/locations.html:5
+msgid "Locations"
+msgstr "Mevkiler"
+
+#: ablog/post.py:565 ablog/post.py:632
+msgid "Posts from"
+msgstr "Mevki"
+
+#: ablog/post.py:566 ablog/templates/ablog/languages.html:4
+#: ablog/templates/languages.html:5
+msgid "Languages"
+msgstr "Diller"
+
+#: ablog/post.py:566 ablog/post.py:567 ablog/post.py:633 ablog/post.py:634
+msgid "Posts in"
+msgstr "Kategori"
+
+#: ablog/post.py:567 ablog/templates/ablog/categories.html:4
+#: ablog/templates/categories.html:5
+msgid "Categories"
+msgstr "Kategoriler"
+
+#: ablog/post.py:568
+msgid "All posts"
+msgstr "Bütün yazılar"
+
+#: ablog/post.py:568 ablog/post.py:635
+msgid "Posted in"
+msgstr "Sene"
+
+#: ablog/post.py:569 ablog/templates/ablog/postcard2.html:117
+#: ablog/templates/ablog/tagcloud.html:3 ablog/templates/postcard2.html:118
+#: ablog/templates/tagcloud.html:4
+msgid "Tags"
+msgstr "Etiketler"
+
+#: ablog/post.py:569 ablog/post.py:636
+msgid "Posts tagged"
+msgstr "Etiket"
+
+#: ablog/post.py:594
+#, fuzzy
+msgid "All Posts"
+msgstr "Bütün yazılar"
+
+#: ablog/post.py:595
+msgid "All"
+msgstr ""
+
+#: ablog/post.py:603
+msgid "Drafts"
+msgstr "Taslaklar"
+
+#: ablog/templates/ablog/archives.html:4 ablog/templates/archives.html:5
+msgid "Archives"
+msgstr "Arşiv"
+
+#: ablog/templates/ablog/collection.html:56 ablog/templates/collection.html:57
+msgid "Read more ..."
+msgstr "Okumaya devam et ..."
+
+#: ablog/templates/ablog/postcard2.html:8 ablog/templates/postcard2.html:9
+msgid "Update"
+msgstr "Güncelleme"
+
+#: ablog/templates/ablog/postcard2.html:20 ablog/templates/postcard2.html:21
+msgid "Author"
+msgstr "Yazar"
+
+#: ablog/templates/ablog/postcard2.html:44 ablog/templates/postcard2.html:45
+msgid "Location"
+msgstr "Mevki"
+
+#: ablog/templates/ablog/postcard2.html:68 ablog/templates/postcard2.html:69
+msgid "Language"
+msgstr "Dil"
+
+#: ablog/templates/ablog/postcard2.html:92 ablog/templates/postcard2.html:93
+msgid "Category"
+msgstr "Kategori"
+
+#: ablog/templates/ablog/postcard2.html:123 ablog/templates/postcard2.html:124
+msgid "Tag"
+msgstr "Etiket"
+
+#: ablog/templates/ablog/postnavy.html:8 ablog/templates/postnavy.html:9
+msgid "Previous"
+msgstr "Önceki"
+
+#: ablog/templates/ablog/postnavy.html:22 ablog/templates/postnavy.html:23
+msgid "Next"
+msgstr "Sonraki"
+
+#: ablog/templates/ablog/recentposts.html:4 ablog/templates/recentposts.html:5
+msgid "Recent Posts"
+msgstr "Yeni Yazılar"
diff --git a/src/ablog/locales/zh_CN/LC_MESSAGES/sphinx.mo b/src/ablog/locales/zh_CN/LC_MESSAGES/sphinx.mo
new file mode 100644
index 0000000..faf4cbd
--- /dev/null
+++ b/src/ablog/locales/zh_CN/LC_MESSAGES/sphinx.mo
Binary files differ
diff --git a/src/ablog/locales/zh_CN/LC_MESSAGES/sphinx.po b/src/ablog/locales/zh_CN/LC_MESSAGES/sphinx.po
new file mode 100644
index 0000000..32d90c6
--- /dev/null
+++ b/src/ablog/locales/zh_CN/LC_MESSAGES/sphinx.po
@@ -0,0 +1,130 @@
+# Chinese (Simplified, China) translations for ablog.
+# Copyright (C) 2020 ORGANIZATION
+# This file is distributed under the same license as the ablog project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: ablog 0.10.4.dev6+g5473a00.d20200325\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2022-11-14 16:46-0800\n"
+"PO-Revision-Date: 2020-03-25 16:35+0800\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: zh_Hans_CN\n"
+"Language-Team: zh_Hans_CN <LL@li.org>\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.11.0\n"
+
+#: ablog/post.py:272
+#, fuzzy
+msgid "Updated on "
+msgstr "更新"
+
+#: ablog/post.py:564 ablog/templates/ablog/authors.html:3
+#: ablog/templates/authors.html:4
+msgid "Authors"
+msgstr "作者"
+
+#: ablog/post.py:564 ablog/post.py:631
+msgid "Posts by"
+msgstr "文章"
+
+#: ablog/post.py:565 ablog/templates/ablog/locations.html:4
+#: ablog/templates/locations.html:5
+msgid "Locations"
+msgstr "地点"
+
+#: ablog/post.py:565 ablog/post.py:632
+msgid "Posts from"
+msgstr "文章自"
+
+#: ablog/post.py:566 ablog/templates/ablog/languages.html:4
+#: ablog/templates/languages.html:5
+msgid "Languages"
+msgstr "语言"
+
+#: ablog/post.py:566 ablog/post.py:567 ablog/post.py:633 ablog/post.py:634
+msgid "Posts in"
+msgstr "文章:"
+
+#: ablog/post.py:567 ablog/templates/ablog/categories.html:4
+#: ablog/templates/categories.html:5
+msgid "Categories"
+msgstr "类别"
+
+#: ablog/post.py:568
+msgid "All posts"
+msgstr "所有文章"
+
+#: ablog/post.py:568 ablog/post.py:635
+msgid "Posted in"
+msgstr "文章:"
+
+#: ablog/post.py:569 ablog/templates/ablog/postcard2.html:117
+#: ablog/templates/ablog/tagcloud.html:3 ablog/templates/postcard2.html:118
+#: ablog/templates/tagcloud.html:4
+msgid "Tags"
+msgstr "标签"
+
+#: ablog/post.py:569 ablog/post.py:636
+msgid "Posts tagged"
+msgstr "文章标签"
+
+#: ablog/post.py:594
+msgid "All Posts"
+msgstr "所有文章"
+
+#: ablog/post.py:595
+msgid "All"
+msgstr "所有"
+
+#: ablog/post.py:603
+msgid "Drafts"
+msgstr "草稿"
+
+#: ablog/templates/ablog/archives.html:4 ablog/templates/archives.html:5
+msgid "Archives"
+msgstr "归档"
+
+#: ablog/templates/ablog/collection.html:56 ablog/templates/collection.html:57
+msgid "Read more ..."
+msgstr "更多 ..."
+
+#: ablog/templates/ablog/postcard2.html:8 ablog/templates/postcard2.html:9
+msgid "Update"
+msgstr "更新"
+
+#: ablog/templates/ablog/postcard2.html:20 ablog/templates/postcard2.html:21
+msgid "Author"
+msgstr "作者"
+
+#: ablog/templates/ablog/postcard2.html:44 ablog/templates/postcard2.html:45
+msgid "Location"
+msgstr "地点"
+
+#: ablog/templates/ablog/postcard2.html:68 ablog/templates/postcard2.html:69
+msgid "Language"
+msgstr "语言"
+
+#: ablog/templates/ablog/postcard2.html:92 ablog/templates/postcard2.html:93
+msgid "Category"
+msgstr "类别"
+
+#: ablog/templates/ablog/postcard2.html:123 ablog/templates/postcard2.html:124
+msgid "Tag"
+msgstr "标签"
+
+#: ablog/templates/ablog/postnavy.html:8 ablog/templates/postnavy.html:9
+msgid "Previous"
+msgstr "上一篇"
+
+#: ablog/templates/ablog/postnavy.html:22 ablog/templates/postnavy.html:23
+msgid "Next"
+msgstr "下一篇"
+
+#: ablog/templates/ablog/recentposts.html:4 ablog/templates/recentposts.html:5
+msgid "Recent Posts"
+msgstr "最近文章"
diff --git a/src/ablog/post.py b/src/ablog/post.py
new file mode 100644
index 0000000..1b3c6aa
--- /dev/null
+++ b/src/ablog/post.py
@@ -0,0 +1,736 @@
+import os
+import logging
+from string import Formatter
+from datetime import datetime
+
+import jinja2
+from dateutil.parser import parse as date_parser
+from docutils import nodes
+from docutils.parsers.rst import Directive, directives
+from docutils.parsers.rst.directives.admonitions import BaseAdmonition
+from feedgen.feed import FeedGenerator
+from sphinx.locale import get_translation
+from sphinx.transforms import SphinxTransform
+from sphinx.util.nodes import set_source_info
+
+import ablog
+from ablog.blog import Blog, os_path_join, revise_pending_xrefs, slugify
+
+__all__ = [
+ "PostNode",
+ "PostList",
+ "UpdateNode",
+ "PostDirective",
+ "UpdateDirective",
+ "PostListDirective",
+ "CheckFrontMatter",
+ "purge_posts",
+ "process_posts",
+ "process_postlist",
+ "generate_archive_pages",
+ "generate_atom_feeds",
+ "register_posts",
+]
+
+# Name used for the *.pot, *.po and *.mo files
+MESSAGE_CATALOG_NAME = "sphinx"
+_ = get_translation(MESSAGE_CATALOG_NAME) # NOQA
+
+
+def _split(a):
+ return [s.strip() for s in (a or "").split(",")]
+
+
+class PostNode(nodes.Element):
+ """
+ Represent ``post`` directive content and options in document tree.
+ """
+
+
+class PostList(nodes.General, nodes.Element):
+ """
+ Represent ``postlist`` directive converted to a list of links.
+ """
+
+
+class UpdateNode(nodes.admonition):
+ """
+ Represent ``update`` directive.
+ """
+
+
+class PostDirective(Directive):
+ """
+ Handle ``post`` directives.
+ """
+
+ has_content = True
+ required_arguments = 0
+ optional_arguments = 1
+ final_argument_whitespace = True
+ option_spec = {
+ "tags": _split,
+ "author": _split,
+ "category": _split,
+ "location": _split,
+ "language": _split,
+ "redirect": _split,
+ "title": lambda a: a.strip(),
+ "image": int,
+ "excerpt": int,
+ "exclude": directives.flag,
+ "nocomments": directives.flag,
+ "canonical_link": str,
+ "external_link": str,
+ }
+
+ def run(self):
+ node = PostNode()
+ node.document = self.state.document
+ set_source_info(self, node)
+ self.state.nested_parse(self.content, self.content_offset, node, match_titles=1)
+ node = _update_post_node(node, self.options, self.arguments)
+ return [node]
+
+
+class UpdateDirective(BaseAdmonition):
+ required_arguments = 1
+ node_class = UpdateNode
+
+ def run(self):
+ ad = super().run()
+ ad[0]["date"] = self.arguments[0] if self.arguments else ""
+ return ad
+
+
+class PostListDirective(Directive):
+ """
+ Handle ``postlist`` directives.
+ """
+
+ has_content = False
+ required_arguments = 0
+ optional_arguments = 1
+ final_argument_whitespace = False
+ option_spec = {
+ "tags": _split,
+ "author": _split,
+ "category": _split,
+ "location": _split,
+ "language": _split,
+ "format": lambda a: a.strip(),
+ "date": lambda a: a.strip(),
+ "sort": directives.flag,
+ "excerpts": directives.flag,
+ "list-style": lambda a: a.strip(),
+ "expand": directives.unchanged,
+ }
+
+ def run(self):
+ node = PostList()
+ node.document = self.state.document
+ set_source_info(self, node)
+ self.state.nested_parse(self.content, self.content_offset, node, match_titles=1)
+ node["length"] = int(self.arguments[0]) if self.arguments else None
+ node["tags"] = self.options.get("tags", [])
+ node["author"] = self.options.get("author", [])
+ node["category"] = self.options.get("category", [])
+ node["location"] = self.options.get("location", [])
+ node["language"] = self.options.get("language", [])
+ node["format"] = self.options.get("format", "{date} - {title}")
+ node["date"] = self.options.get("date", None)
+ node["sort"] = "sort" in self.options
+ node["excerpts"] = "excerpts" in self.options
+ node["image"] = "image" in self.options
+ node["list-style"] = self.options.get("list-style", "none")
+ node["expand"] = self.options.get("expand", None)
+ return [node]
+
+
+class CheckFrontMatter(SphinxTransform):
+ """
+ Check the doctree for frontmatter meant for a blog post.
+
+ This is mutually-exclusive with the PostDirective. Only one much be
+ used.
+ """
+
+ # Priority before 880 so that it runs before the `doctree-read` event
+ default_priority = 800
+
+ def apply(self):
+ # Check if page-level metadata has been given
+ docinfo = list(self.document.findall(nodes.docinfo))
+ if not docinfo:
+ return None
+ docinfo = docinfo[0]
+ # Pull the metadata for the page to check if it is a blog post
+ metadata = {fn.children[0].astext(): fn.children[1].astext() for fn in docinfo.findall(nodes.field)}
+ tags = metadata.get("tags")
+ if isinstance(tags, str):
+ # myst_parser store front-matter field to TextNode in dict_to_fm_field_list.
+ # like ["a", "b", "c"]
+ # remove [] and quotes
+ tags = tags.strip().lstrip("[").rstrip("]")
+ metadata["tags"] = ",".join(
+ [t.strip().lstrip('"').lstrip("'").rstrip('"').rstrip("'") for t in tags.split(",")]
+ )
+ if list(docinfo.findall(nodes.author)):
+ metadata["author"] = list(docinfo.findall(nodes.author))[0].astext()
+ # These two fields are special-cased in docutils
+ if list(docinfo.findall(nodes.date)):
+ metadata["date"] = list(docinfo.findall(nodes.date))[0].astext()
+ if "blogpost" not in metadata and self.env.docname not in self.config.matched_blog_posts:
+ return None
+ for node in self.document.findall(PostNode):
+ if node:
+ logging.warning("Found blog post front-matter as well as post directive, using post directive.")
+ # Iterate through metadata and create a PostNode with relevant fields
+ option_spec = PostDirective.option_spec
+ for key, val in metadata.items():
+ if key in option_spec:
+ if callable(option_spec[key]):
+ new_val = option_spec[key](val)
+ elif isinstance(option_spec[key], directives.flag):
+ new_val = True
+ metadata[key] = new_val
+ node = PostNode()
+ node.document = self.document
+ node = _update_post_node(node, metadata, [])
+ node["date"] = metadata.get("date")
+ if not metadata.get("excerpt"):
+ blog = Blog(self.app)
+ node["excerpt"] = blog.post_auto_excerpt
+ sections = list(self.document.findall(nodes.section))
+ if sections:
+ sections[0].children.append(node)
+ node.parent = sections[0]
+
+
+def purge_posts(app, env, docname):
+ """
+ Remove post and reference to it from the standard domain when its document
+ is removed or changed.
+ """
+
+ if hasattr(env, "ablog_posts"):
+ env.ablog_posts.pop(docname, None)
+ filename = os.path.split(docname)[1]
+ env.domains["std"].data["labels"].pop(filename, None)
+ env.domains["std"].data["anonlabels"].pop(filename, None)
+
+
+def _update_post_node(node, options, arguments):
+ """
+ Extract metadata from options and populate a post node.
+ """
+ node["date"] = arguments[0] if arguments else None
+ node["tags"] = options.get("tags", [])
+ node["author"] = options.get("author", [])
+ node["category"] = options.get("category", [])
+ node["location"] = options.get("location", [])
+ node["language"] = options.get("language", [])
+ node["redirect"] = options.get("redirect", [])
+ node["title"] = options.get("title", None)
+ node["image"] = options.get("image", None)
+ node["excerpt"] = options.get("excerpt", None)
+ node["exclude"] = "exclude" in options
+ node["nocomments"] = "nocomments" in options
+ node["canonical_link"] = options.get("canonical_link", [])
+ node["external_link"] = options.get("external_link", [])
+ return node
+
+
+def _get_section_title(section):
+ """
+ Return section title as text.
+ """
+ for title in section.findall(nodes.title):
+ return title.astext()
+ raise Exception("Missing title")
+ # A problem with the following is that title may contain pending
+ # references, e.g. :ref:`tag-tips`
+
+
+def _get_update_dates(section, docname, post_date_format):
+ """
+ Return list of dates of updates found section.
+ """
+ update_nodes = list(section.findall(UpdateNode))
+ update_dates = []
+ for update_node in update_nodes:
+ try:
+ update = datetime.strptime(update_node["date"], post_date_format)
+ except ValueError:
+ if date_parser:
+ try:
+ update = date_parser(update_node["date"])
+ except ValueError:
+ raise ValueError("invalid post date in: " + docname)
+ else:
+ raise ValueError(
+ f"invalid post date ({update_node['date']}) in "
+ + docname
+ + f". Expected format: {post_date_format}"
+ )
+ # Insert a new title element which contains the `Updated on {date}` logic.
+ substitute = nodes.title("", _("Updated on ") + update.strftime(post_date_format))
+ update_node.insert(0, substitute)
+ update_node["classes"] = ["note", "update"]
+ update_dates.append(update)
+ return update_dates
+
+
+def process_posts(app, doctree):
+ """
+ Process posts and map posted document names to post details in the
+ environment.
+ """
+ env = app.builder.env
+ if not hasattr(env, "ablog_posts"):
+ env.ablog_posts = {}
+ post_nodes = list(doctree.findall(PostNode))
+ if not post_nodes:
+ return
+ post_date_format = app.config["post_date_format"]
+ should_auto_orphan = app.config["post_auto_orphan"]
+ docname = env.docname
+ if should_auto_orphan:
+ # mark the post as 'orphan' so that
+ # "document isn't included in any toctree" warning is not issued
+ # We do not simply assign to should_auto_orphan because if auto-orphan
+ # is false, we still want to respect the per-post :rst:dir`orphan` setting
+ app.env.metadata[docname]["orphan"] = True
+ blog = Blog(app)
+ auto_excerpt = blog.post_auto_excerpt
+ multi_post = len(post_nodes) > 1 or blog.post_always_section
+ for order, node in enumerate(post_nodes, start=1):
+ if node["excerpt"] is None:
+ node["excerpt"] = auto_excerpt
+ if multi_post:
+ # section title, and first few paragraphs of the section of post
+ # are used when there are more than 1 posts
+ section = node
+ while True:
+ if isinstance(section, nodes.section):
+ break
+ section = node.parent
+ else:
+ section = doctree
+ # get updates here, in the section that post belongs to
+ # Might there be orphan updates?
+ update_dates = _get_update_dates(section, docname, post_date_format)
+ # Making sure that post has a title because all post titles
+ # are needed when resolving post lists in documents
+ title = node["title"] or _get_section_title(section)
+ # creating a summary here, before references are resolved
+ excerpt = []
+ if node.children:
+ if node["exclude"]:
+ node.replace_self([])
+ else:
+ node.replace_self(node.children)
+ for child in node.children:
+ excerpt.append(child.deepcopy())
+ elif node["excerpt"]:
+ count = 0
+ for nod in section.findall(nodes.paragraph):
+ excerpt.append(nod.deepcopy())
+ count += 1
+ if count >= (node["excerpt"] or 0):
+ break
+ node.replace_self([])
+ else:
+ node.replace_self([])
+ nimg = node["image"] or blog.post_auto_image
+ if nimg:
+ for img, nod in enumerate(section.findall(nodes.image), start=1):
+ if img == nimg:
+ excerpt.append(nod.deepcopy())
+ break
+ date = node["date"]
+ if date:
+ try:
+ date = datetime.strptime(date, post_date_format)
+ except ValueError:
+ if date_parser:
+ try:
+ date = date_parser(date)
+ except ValueError:
+ raise ValueError("invalid post date in: " + docname)
+ else:
+ raise ValueError(
+ f"invalid post date ({date}) in " + docname + f". Expected format: {post_date_format}"
+ )
+ else:
+ date = None
+ # if docname ends with `index` use folder name to reference the document
+ # a potential problem here is that there may be files/folders with the
+ # same name, so issuing a warning when that's the case may be a good idea
+ folder, label = os.path.split(docname)
+ if label == "index":
+ folder, label = os.path.split(folder)
+ if not label:
+ label = slugify(title)
+ section_name = ""
+ if multi_post and section.parent is not doctree:
+ section_name = section.attributes["ids"][0]
+ label += "-" + section_name
+ else:
+ # create a reference for the post
+ # if it is posting the document
+ # ! this does not work for sections
+ app.env.domains["std"].data["labels"][label] = (docname, label, title)
+ app.env.domains["std"].data["anonlabels"][label] = (docname, label)
+ if section.parent is doctree:
+ section_copy = section[0].deepcopy()
+ else:
+ section_copy = section.deepcopy()
+ # multiple posting may result having post nodes
+ for nn in section_copy.findall(PostNode):
+ if nn["exclude"]:
+ nn.replace_self([])
+ else:
+ nn.replace_self(node.children)
+ postinfo = {
+ "docname": docname,
+ "section": section_name,
+ "order": order,
+ "date": date,
+ "update": max(update_dates + [date]),
+ "title": title,
+ "excerpt": excerpt,
+ "tags": node["tags"],
+ "author": node["author"],
+ "category": node["category"],
+ "location": node["location"],
+ "language": node["language"],
+ "redirect": node["redirect"],
+ "nocomments": node["nocomments"],
+ "image": node["image"],
+ "exclude": node["exclude"],
+ "canonical_link": node["canonical_link"],
+ "external_link": node["external_link"],
+ "doctree": section_copy,
+ }
+ if docname not in env.ablog_posts:
+ env.ablog_posts[docname] = []
+ env.ablog_posts[docname].append(postinfo)
+ # instantiate catalogs and collections here
+ # so that references are created and no warnings are issued
+ if app.builder.format == "html":
+ stdlabel = env.domains["std"].data["labels"] # NOQA
+ else:
+ if hasattr(env, "intersphinx_inventory"):
+ stdlabel = env.intersphinx_inventory.setdefault("std:label", {}) # NOQA
+ baseurl = getattr(env.config, "blog_baseurl").rstrip("/") + "/" # NOQA
+ project, version = env.config.project, str(env.config.version) # NOQA
+ for key in ["tags", "author", "category", "location", "language"]:
+ catalog = blog.catalogs[key]
+ for label in postinfo[key]:
+ coll = catalog[label] # NOQA
+ if postinfo["date"]:
+ coll = blog.archive[postinfo["date"].year] # NOQA
+
+
+def process_postlist(app, doctree, docname):
+ """
+ Replace `PostList` nodes with lists of posts.
+
+ Also, register all posts if they have not been registered yet.
+ """
+ blog = Blog(app)
+ if not blog:
+ register_posts(app)
+ for node in doctree.findall(PostList):
+ colls = []
+ for cat in ["tags", "author", "category", "location", "language"]:
+ for coll in node[cat]:
+ if coll in blog.catalogs[cat].collections:
+ colls.append(blog.catalogs[cat].collections[coll])
+ if colls:
+ posts = set(blog.posts)
+ for coll in colls:
+ posts = posts & set(coll)
+ posts = list(posts)
+ posts.sort(reverse=True)
+ posts = posts[: node.attributes["length"]]
+ else:
+ posts = list(blog.recent(node.attributes["length"], docname, **node.attributes))
+ if node.attributes["sort"]:
+ posts.sort() # in reverse chronological order, so no reverse=True
+ fmts = list(Formatter().parse(node.attributes["format"]))
+ not_in = {"date", "title", "author", "location", "language", "category", "tags", None}
+ for text, key, __, __ in fmts:
+ if key not in not_in:
+ raise KeyError(f"{key} is not recognized in postlist format")
+ excerpts = node.attributes["excerpts"]
+ expand = node.attributes["expand"]
+ date_format = node.attributes["date"] or _(blog.post_date_format_short)
+ bl = nodes.bullet_list()
+ bl.attributes["classes"].append("postlist-style-" + node["list-style"])
+ bl.attributes["classes"].append("postlist")
+ for post in posts:
+ bli = nodes.list_item()
+ bli.attributes["classes"].append("ablog-post")
+ bl.append(bli)
+ par = nodes.paragraph()
+ bli.append(par)
+ for text, key, __, __ in fmts:
+ if text:
+ par.append(nodes.Text(text))
+ if key is None:
+ continue
+ if key == "date":
+ par.append(nodes.Text(post.date.strftime(date_format)))
+ else:
+ if key == "title":
+ items = [post]
+ else:
+ items = getattr(post, key)
+
+ for i, item in enumerate(items, start=1):
+ if key == "title":
+ ref = nodes.reference()
+ if item.options.get("external_link"):
+ ref["refuri"] = post.options.get("external_link")
+ else:
+ ref["refuri"] = app.builder.get_relative_uri(docname, item.docname)
+ ref["internal"] = True
+ ref["ids"] = []
+ ref["backrefs"] = []
+ ref["dupnames"] = []
+ ref["classes"] = []
+ ref["names"] = []
+ ref.append(nodes.Text(str(item)))
+ par.attributes["classes"].append("ablog-post-title")
+ else:
+ ref = _missing_reference(app, item.xref, docname)
+ par.append(ref)
+ if i < len(items):
+ par.append(nodes.Text(", "))
+ if excerpts and post.excerpt:
+ for enode in post.excerpt:
+ enode = enode.deepcopy()
+ enode.attributes["classes"].append("ablog-post-excerpt")
+ revise_pending_xrefs(enode, docname)
+ app.env.resolve_references(enode, docname, app.builder)
+ enode.parent = bli.parent
+ bli.append(enode)
+ if expand:
+ ref = app.builder.get_relative_uri(docname, post.docname)
+ enode = nodes.paragraph()
+ enode.attributes["classes"].append("ablog-post-expand")
+ refnode = nodes.reference("", "", internal=True, refuri=ref)
+ innernode = nodes.emphasis(text=expand)
+ refnode.append(innernode)
+ enode.append(refnode)
+ bli.append(enode)
+ node.replace_self(bl)
+
+
+def missing_reference(app, env, node, contnode):
+ target = node["reftarget"]
+ logging.debug(f"missing reference: {target}, {contnode}")
+ return _missing_reference(app, target, node.get("refdoc"), contnode, node.get("refexplicit"))
+
+
+def _missing_reference(app, target, refdoc, contnode=None, refexplicit=False):
+ blog = Blog(app)
+ if target in blog.references:
+ docname, dispname = blog.references[target]
+ # Avoid adding html to the atom.xml
+ if "html" in app.builder.name and "atom.xml" not in docname:
+ internal = True
+ uri = app.builder.get_relative_uri(refdoc, docname)
+ else:
+ internal = False
+ uri = blog.blog_baseurl.rstrip("/") + "/" + docname
+ newnode = nodes.reference("", "", internal=internal, refuri=uri, reftitle=dispname)
+ if refexplicit:
+ newnode.append(contnode)
+ else:
+ emp = nodes.emphasis()
+ newnode.append(emp)
+ emp.append(nodes.Text(str(dispname)))
+ return newnode
+
+
+def generate_archive_pages(app):
+ """
+ Generate archive pages for all posts, categories, tags, authors, and
+ drafts.
+ """
+ if not ablog.builder_support(app):
+ return
+ blog = Blog(app)
+ for post in blog.posts:
+ for redirect in post.redirect:
+ yield (redirect, {"redirect": post.docname, "post": post}, "ablog/redirect.html")
+ found_docs = app.env.found_docs
+ atom_feed = bool(blog.blog_baseurl)
+ feed_archives = blog.blog_feed_archives
+ blog_path = blog.blog_path
+ for title, header, catalog in [
+ (_("Authors"), _("Posts by"), blog.author),
+ (_("Locations"), _("Posts from"), blog.location),
+ (_("Languages"), _("Posts in"), blog.language),
+ (_("Categories"), _("Posts in"), blog.category),
+ (_("All posts"), _("Posted in"), blog.archive),
+ (_("Tags"), _("Posts tagged"), blog.tags),
+ ]:
+ if not catalog:
+ continue
+ context = {"parents": [], "title": title, "header": header, "catalog": catalog, "summary": True}
+ if catalog.docname not in found_docs:
+ yield (catalog.docname, context, "ablog/catalog.html")
+ for collection in catalog:
+ if not collection:
+ continue
+ context = {
+ "parents": [],
+ "title": f"{header} {collection}",
+ "header": header,
+ "collection": collection,
+ "summary": True,
+ "feed_path": collection.path if feed_archives else blog_path,
+ "archive_feed": atom_feed and feed_archives,
+ }
+ context["feed_title"] = context["title"]
+ if collection.docname not in found_docs:
+ yield (collection.docname, context, "ablog/collection.html")
+ if 1:
+ context = {
+ "parents": [],
+ "title": _("All Posts"),
+ "header": _("All"),
+ "collection": blog.posts,
+ "summary": True,
+ "atom_feed": atom_feed,
+ "feed_path": blog.blog_path,
+ }
+ docname = blog.posts.docname
+ yield (docname, context, "ablog/collection.html")
+ context = {"parents": [], "title": _("Drafts"), "collection": blog.drafts, "summary": True}
+ yield (blog.drafts.docname, context, "ablog/collection.html")
+
+
+def generate_atom_feeds(app):
+ """
+ Generate archive pages for all posts, categories, tags, authors, and
+ drafts.
+ """
+ if not ablog.builder_support(app):
+ return
+ blog = Blog(app)
+ base_url = blog.blog_baseurl
+ if not base_url:
+ return
+ feeds = [
+ (
+ blog.posts,
+ blog.blog_path,
+ os.path.join(app.builder.outdir, blog.blog_path, feed_root + ".xml"),
+ blog.blog_title,
+ os_path_join(base_url, blog.blog_path, feed_root + ".xml"),
+ feed_templates,
+ )
+ for feed_root, feed_templates in blog.blog_feed_templates.items()
+ ]
+ if blog.blog_feed_archives:
+ for header, catalog in [
+ (_("Posts by"), blog.author),
+ (_("Posts from"), blog.location),
+ (_("Posts in"), blog.language),
+ (_("Posts in"), blog.category),
+ (_("Posted in"), blog.archive),
+ (_("Posts tagged"), blog.tags),
+ ]:
+ for coll in catalog:
+ # skip collections containing only drafts
+ if not len(coll):
+ continue
+ folder = os.path.join(app.builder.outdir, coll.path)
+ if not os.path.isdir(folder):
+ os.makedirs(folder)
+ for feed_root, feed_templates in blog.blog_feed_templates.items():
+ feeds.append(
+ (
+ coll,
+ coll.path,
+ os.path.join(folder, feed_root + ".xml"),
+ blog.blog_title + " - " + header + " " + str(coll),
+ os_path_join(base_url, coll.path, feed_root + ".xml"),
+ feed_templates,
+ )
+ )
+ # Config options
+ feed_length = blog.blog_feed_length
+ feed_fulltext = blog.blog_feed_fulltext
+ for feed_posts, pagename, feed_path, feed_title, feed_url, feed_templates in feeds:
+ feed = FeedGenerator()
+ feed.id(blog.blog_baseurl)
+ feed.title(feed_title)
+ feed.link(href=base_url)
+ feed.subtitle(blog.blog_feed_subtitle)
+ feed.link(href=feed_url, rel="self")
+ feed.language(app.config.language)
+ feed.generator("ABlog", ablog.__version__, "https://ablog.readthedocs.io/")
+ sorted_posts_by_date = sorted(feed_posts, key=lambda post: post.date, reverse=True)
+ for i, post in enumerate(sorted_posts_by_date):
+ if feed_length and i == feed_length:
+ break
+ post_url = os_path_join(base_url, app.builder.get_target_uri(post.docname))
+ if post.section:
+ post_url += "#" + post.section
+ if blog.blog_feed_titles:
+ content = None
+ else:
+ content = post.to_html(pagename, fulltext=feed_fulltext, img_url=True)
+ feed_entry = feed.add_entry(order="append")
+ feed_entry.id(post_url)
+ feed_entry.link(href=post_url)
+ feed_entry.author({"name": author.name for author in post.author})
+ feed_entry.pubDate(post.date.astimezone())
+ feed_entry.updated(post.update.astimezone())
+ for tag in sorted(post.tags):
+ feed_entry.category(
+ dict(
+ term=tag.name.strip().replace(" ", ""),
+ label=tag.label,
+ )
+ )
+ # Entry values that support templates
+ title = post.title
+ summary = "".join(paragraph.astext() for paragraph in post.excerpt)
+ template_values = {}
+ for element in ("title", "summary", "content"):
+ if element in feed_templates:
+ template_values[element] = jinja2.Template(feed_templates[element]).render(**locals())
+ feed_entry.title(template_values.get("title", title))
+ summary = template_values.get("summary", summary)
+ if summary:
+ feed_entry.summary(summary)
+ content = template_values.get("content", content)
+ if content:
+ feed_entry.content(content=content, type="html")
+ parent_dir = os.path.dirname(feed_path)
+ if not os.path.isdir(parent_dir):
+ os.makedirs(parent_dir)
+ with open(feed_path, "w", encoding="utf-8") as out:
+ feed_str = feed.atom_str(pretty=True)
+ out.write(feed_str.decode())
+ if 0:
+ # this is to make the function a generator
+ # and make work for Sphinx 'html-collect-pages'
+ yield
+
+
+def register_posts(app):
+ """
+ Register posts found in the Sphinx build environment.
+ """
+ blog = Blog(app)
+ for docname, posts in getattr(app.env, "ablog_posts", {}).items():
+ for postinfo in posts:
+ blog.register(docname, postinfo)
diff --git a/src/ablog/start.py b/src/ablog/start.py
new file mode 100644
index 0000000..cc17e94
--- /dev/null
+++ b/src/ablog/start.py
@@ -0,0 +1,617 @@
+import sys
+import time
+import datetime
+from os import path
+from textwrap import wrap
+
+from docutils.utils import column_width
+from sphinx.cmd.quickstart import do_prompt, is_path
+from sphinx.util import texescape
+from sphinx.util.console import bold, color_terminal, nocolor
+from sphinx.util.osutil import ensuredir, make_filename
+
+from ablog import __version__
+
+
+def w(t, ls=80):
+ return "\n".join(wrap(t, ls))
+
+
+__all__ = ["generate", "ask_user", "ablog_start"]
+
+ABLOG_CONF = "#!/usr/bin/env python\n"
+ABLOG_CONF += """
+
+# {project} build configuration file, created by
+# `ablog start` on {now}.
+#
+# Note that not all possible configuration values are present in this file.
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import os
+import sys
+import ablog
+import alabaster
+
+# -- General ABlog Options ----------------------------------------------------
+
+# A path relative to the configuration directory for blog archive pages.
+# blog_path = 'blog'
+
+# The "title" for the blog, used in active pages. Default is ``'Blog'``.
+blog_title = "{project} Blog"
+
+# Base URL for the website, required for generating feeds.
+# e.g. blog_baseurl = "http://example.com/"
+blog_baseurl = "{blog_baseurl}"
+
+# Choose to archive only post titles. Archiving only titles can speed
+# up project building.
+# blog_archive_titles = False
+
+# -- Blog Authors, Languages, and Locations -----------------------------------
+
+# A dictionary of author names mapping to author full display names and
+# links. Dictionary keys are what should be used in ``post`` directive
+# to refer to the author. Default is ``{{}}``.
+blog_authors = {{
+ "{author}": ("{author}", None),
+}}
+
+
+# A dictionary of language code names mapping to full display names and
+# links of these languages. Similar to :confval:`blog_authors`, dictionary
+# keys should be used in ``post`` directive to refer to the locations.
+# Default is ``{{}}``.
+# blog_languages = {{
+# 'en': ('English', None),
+# }}
+
+
+# A dictionary of location names mapping to full display names and
+# links of these locations. Similar to :confval:`blog_authors`, dictionary
+# keys should be used in ``post`` directive to refer to the locations.
+# Default is ``{{}}``.
+# blog_locations = {{
+# 'Earth': ('The Blue Planet', 'https://en.wikipedia.org/wiki/Earth),
+# }}
+
+# This will prevent ablog from injecting its own templates into the Sphinx
+# build. This is only useful when you have a custom template bridge (rare).
+# See https://github.com/sunpy/ablog/pull/144 for the full context.
+# skip_injecting_base_ablog_templates = False
+
+# -- Blog Post Related --------------------------------------------------------
+
+# Format date for a post.
+# post_date_format = '%%b %%d, %%Y'
+
+# Number of paragraphs (default is ``1``) that will be displayed as an excerpt
+# from the post. Setting this ``0`` will result in displaying no post excerpt
+# in archive pages. This option can be set on a per post basis using
+# post_auto_excerpt = 1
+
+# Index of the image that will be displayed in the excerpt of the post.
+# Default is ``0``, meaning no image. Setting this to ``1`` will include
+# the first image, when available, to the excerpt. This option can be set
+# on a per post basis using :rst:dir:`post` directive option ``image``.
+# post_auto_image = 0
+
+# Number of seconds (default is ``5``) that a redirect page waits before
+# refreshing the page to redirect to the post.
+# post_redirect_refresh = 5
+
+# When ``True``, post title and excerpt is always taken from the section that
+# contains the :rst:dir:`post` directive, instead of the document. This is the
+# behavior when :rst:dir:`post` is used multiple times in a document. Default
+# is ``False``.
+# post_always_section = False
+
+# When ``True``, links to the previous and next posts will be rendered at the
+# bottom of the page.
+# Default is ``True``
+# post_show_prev_next = True
+
+# When ``False``, the :rst:dir:`orphan` directive is not automatically set
+# for each post. Without this directive, Sphinx will warn about posts that
+# are not explicitly referenced via another document. :rst:dir:`orphan` can
+# be set on a per-post basis as well if this is false. Default is ``True``.
+# post_auto_orphan = True
+
+# -- ABlog Sidebars -------------------------------------------------------
+
+# There are seven sidebars you can include in your HTML output.
+# postcard.html provides information regarding the current post.
+# recentposts.html lists most recent five posts. Others provide
+# a link to a archive pages generated for each tag, category, and year.
+# In addition, there are authors.html, languages.html, and locations.html
+# sidebars that link to author and location archive pages.
+html_sidebars = {{
+'**': [ 'ablog/postcard.html', 'navigation.html',
+ 'ablog/recentposts.html', 'ablog/tagcloud.html',
+ 'ablog/categories.html', 'ablog/archives.html',
+ 'searchfield.html',
+ ],
+ }}
+
+# -- Blog Feed Options --------------------------------------------------------
+
+# Turn feeds by setting :confval:`blog_baseurl` configuration variable.
+# Choose to create feeds per author, location, tag, category, and year,
+# default is ``False``.
+# blog_feed_archives = False
+
+# Choose to display full text in blog feeds, default is ``False``.
+# blog_feed_fulltext = False
+
+# Blog feed subtitle, default is ``None``.
+# blog_feed_subtitle = None
+
+# Choose to feed only post titles, default is ``False``.
+# blog_feed_titles = False
+
+# Specify custom Jinja2 templates for feed entry elements:
+# `title`, `summary`, or `content`
+# For example, to add an additional feed for posting to social media:
+# blog_feed_templates = {{
+# # Use defaults, no templates
+# "atom": {{}},
+# # Create content text suitable posting to social media
+# "social": {{
+# # Format tags as hashtags and append to the content
+# "content": "{{ title }}{{% for tag in post.tags %}}"
+# " #{{ tag.name|trim()|replace(' ', '') }}"
+# "{{% endfor %}}",
+# }},
+# }}
+# Default: Create one `atom.xml` feed without any templates
+# blog_feed_templates = {{"atom": {{}} }}
+
+# Specify number of recent posts to include in feeds, default is ``None``
+# for all posts.
+# blog_feed_length = None
+
+# -- Font-Awesome Options -----------------------------------------------------
+
+# ABlog templates will use of Font Awesome icons if one of the following
+# is ``True``
+
+# Link to `Font Awesome`_ at `Bootstrap CDN`_ and use icons in sidebars
+# and post footers. Default: ``None``
+# fontawesome_link_cdn = None
+
+# Sphinx_ theme already links to `Font Awesome`_. Default: ``False``
+# fontawesome_included = False
+
+# Alternatively, you can provide the path to `Font Awesome`_ :file:`.css`
+# with the configuration option: fontawesome_css_file
+# Path to `Font Awesome`_ :file:`.css` (default is ``None``) that will
+# be linked to in HTML output by ABlog.
+# fontawesome_css_file = None
+
+# -- Disqus Integration -------------------------------------------------------
+
+# You can enable Disqus_ by setting ``disqus_shortname`` variable.
+# Disqus_ short name for the blog.
+# disqus_shortname = None
+
+# Choose to disqus pages that are not posts, default is ``False``.
+# disqus_pages = False
+
+# Choose to disqus posts that are drafts (without a published date),
+# default is ``False``.
+# disqus_drafts = False
+
+# -- Sphinx Options -----------------------------------------------------------
+
+# If your project needs a minimal Sphinx version, state it here.
+needs_sphinx = '1.2'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.extlinks',
+ 'sphinx.ext.intersphinx',
+ 'sphinx.ext.todo',
+ 'alabaster',
+ 'ablog',
+]
+
+# The suffix(es) of source filenames.
+source_suffix = "{suffix}"
+
+# The encoding of source files.
+# source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = "{master}"
+
+# General information about the project.
+project = "{project}"
+copyright = "{copyright}"
+author = "{author}"
+
+# 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 short X.Y version.
+version = "{version}"
+# The full version, including alpha/beta/rc tags.
+release = "{release}"
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = "{language}"
+
+# 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'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ["{exclude_patterns}"]
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+# default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+# add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+# add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+# show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+# modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+# keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = {ext_todo}
+
+
+# -- 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 = 'alabaster'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+html_theme_options = {{
+ 'github_button': False,
+}}
+
+# Add any paths that contain custom themes here, relative to this directory.
+html_theme_path = [alabaster.get_path()]
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+# html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+# html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+# html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+# html_favicon = None
+
+# 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 = ["{dot}static"]
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+# html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+# html_last_updated_fmt = '%%b %%d, %%Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+# html_use_smartypants = True
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+# html_additional_pages = {{}}
+
+# If false, no module index is generated.
+# html_domain_indices = True
+
+# If false, no index is generated.
+# html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+# html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+# html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+# html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+# html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+# html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+# html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
+# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
+# html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# Now only 'ja' uses this config value
+# html_search_options = {{'type': 'default'}}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+# html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = "{project_fn}doc"
+
+
+"""
+
+ABLOG_INDEX = """
+.. {project} index file, created by `ablog start` on {now}.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Welcome to {author}'s Blog!
+==========={project_underline}=================
+
+Hello World! Find more about me here: :ref:`about`
+
+
+Here is a list of most recent posts:
+
+.. postlist:: 5
+ :excerpts:
+
+
+.. `toctree` directive, below, contains list of non-post `.rst` files.
+ This is how they appear in Navigation sidebar. Note that directive
+ also contains `:hidden:` option so that it is not included inside the page.
+
+ Posts are excluded from this directive so that they aren't double listed
+ in the sidebar both under Navigation and Recent Posts.
+
+.. toctree::
+ :hidden:
+
+ about.rst
+
+"""
+
+ABLOG_ABOUT = """
+.. _about:
+
+About {author}
+============================
+
+The world wants to know more about you.
+
+"""
+
+ABLOG_POST = """
+.. {project} post example, created by `ablog start` on {post_date}.
+
+.. post:: {post_date}
+ :tags: atag
+ :author: {author}
+
+First Post
+==========
+
+World, hello again! This very first paragraph of the post will be used
+as excerpt in archives and feeds. Find out how to control how much is shown
+in `Post Excerpts and Images
+<https://ablog.readthedocs.io/manual/post-excerpts-and-images/>`__. Remember
+that you can refer to posts by file name, e.g. ``:ref:`first-post``` results
+in :ref:`first-post`. Find out more at `Cross-Referencing Blog Pages
+<https://ablog.readthedocs.io/manual/cross-referencing-blog-pages/>`__.
+"""
+
+CONF_DEFAULTS = {
+ "sep": False,
+ "dot": "_",
+ "language": "en",
+ "suffix": ".rst",
+ "master": "index",
+ "makefile": False,
+ "batchfile": False,
+ "epub": False,
+ "ext_todo": False,
+}
+
+
+def generate(d, overwrite=True, silent=False):
+ """
+ Borrowed from Sphinx 1.3b3.
+
+ Generate project based on values in *d*.
+ """
+ texescape.init()
+ if "mastertoctree" not in d:
+ d["mastertoctree"] = ""
+ if "mastertocmaxdepth" not in d:
+ d["mastertocmaxdepth"] = 2
+ d["project_fn"] = make_filename(d["project"])
+ d["project_manpage"] = d["project_fn"].lower()
+ d["now"] = time.asctime()
+ d["project_underline"] = column_width(d["project"]) * "="
+ d["copyright"] = time.strftime("%Y") + ", " + d["author"]
+ d["author_texescaped"] = texescape.escape(str(d["author"]).translate(str(d["author"])))
+ d["project_doc"] = d["project"] + " Documentation"
+ d["project_doc_texescaped"] = texescape.escape(
+ str(d["project"] + " Documentation").translate(str(d["project"] + " Documentation"))
+ )
+ if not path.isdir(d["path"]):
+ ensuredir(d["path"])
+ srcdir = d["sep"] and path.join(d["path"], "source") or d["path"]
+ ensuredir(srcdir)
+ d["exclude_patterns"] = ""
+ ensuredir(path.join(srcdir, d["dot"] + "templates"))
+ ensuredir(path.join(srcdir, d["dot"] + "static"))
+
+ def write_file(fpath, content, newline=None):
+ if overwrite or not path.isfile(fpath):
+ print(f"Creating file {fpath}.")
+ f = open(fpath, "wt", encoding="utf-8", newline=newline)
+ try:
+ f.write(content)
+ finally:
+ f.close()
+ else:
+ print(f"File {fpath} already exists, skipping.")
+
+ conf_text = ABLOG_CONF.format(**d)
+ write_file(path.join(srcdir, "conf.py"), conf_text)
+ masterfile = path.join(srcdir, d["master"] + d["suffix"])
+ write_file(masterfile, ABLOG_INDEX.format(**d))
+ about = path.join(srcdir, "about" + d["suffix"])
+ write_file(about, ABLOG_ABOUT.format(**d))
+ d["post_date"] = datetime.datetime.today().strftime("%b %d, %Y")
+ firstpost = path.join(srcdir, "first-post" + d["suffix"])
+ write_file(firstpost, ABLOG_POST.format(**d))
+ if silent:
+ return
+ print(bold("Finished: An initial directory structure has been created."))
+
+
+def ask_user(d):
+ """
+ Borrowed from Sphinx 1.3b3.
+
+ Ask the user for quickstart values missing from *d*.
+
+ Values are:
+
+ * path: root path
+ * project: project name
+ * author: author names
+ * version: version of project
+ * release: release of project
+ """
+ d.update(CONF_DEFAULTS)
+ print(bold(f"Welcome to the ABlog {__version__} quick start utility."))
+ print("")
+ print(
+ w(
+ "Please enter values for the following settings (just press Enter "
+ "to accept a default value, if one is given in brackets)."
+ )
+ )
+ print("")
+ if "path" in d:
+ print(bold(f"Selected root path: {d['path']}"))
+ else:
+ print("Enter the root path for your blog project (path has to exist).")
+ d["path"] = do_prompt("Root path for your project (path has to exist)", ".", is_path)
+
+ while path.isfile(path.join(d["path"], "conf.py")) or path.isfile(path.join(d["path"], "source", "conf.py")):
+ print("")
+ print(bold(w("Error: an existing conf.py has been found in the " "selected root path.")))
+ print("ablog start will not overwrite existing Sphinx projects.")
+ print("")
+ d["path"] = do_prompt("Please enter a new root path (or just Enter to exit)", ".", is_path)
+ if not d["path"]:
+ sys.exit(1)
+ if "project" not in d:
+ print("")
+ print(
+ w(
+ "Project name will occur in several places in the website, "
+ "including blog archive pages and atom feeds. Later, you can "
+ "set separate names for different parts of the website in "
+ "configuration file."
+ )
+ )
+ d["project"] = do_prompt("Project name")
+ if "author" not in d:
+ print(
+ w(
+ "This of author as the copyright holder of the content. "
+ "If your blog has multiple authors, you might want to enter "
+ "a team name here. Later, you can specify individual authors "
+ "using `blog_authors` configuration option."
+ )
+ )
+ d["author"] = do_prompt("Author name(s)")
+ d["release"] = d["version"] = ""
+ while path.isfile(path.join(d["path"], d["master"] + d["suffix"])) or path.isfile(
+ path.join(d["path"], "source", d["master"] + d["suffix"])
+ ):
+ print("")
+ print(
+ bold(
+ w(
+ f"Error: the master file {d['master'] + d['suffix']} has already been found in the "
+ "selected root path."
+ )
+ )
+ )
+ print("ablog-start will not overwrite the existing file.")
+ print("")
+ d["master"] = do_prompt(
+ w("Please enter a new file name, or rename the " "existing file and press Enter"), d["master"]
+ )
+ if "blog_baseurl" not in d:
+ print("")
+ print(
+ w(
+ "Please enter the base URL for your project. Blog feeds will "
+ "be generated relative to this URL. If you don't have one yet, "
+ "you can set it in configuration file later."
+ )
+ )
+ d["blog_baseurl"] = do_prompt("Base URL for your project", None, lambda x: x)
+ print("")
+
+
+def ablog_start(**kwargs):
+ if not color_terminal():
+ nocolor()
+ d = CONF_DEFAULTS
+ try:
+ ask_user(d)
+ except (KeyboardInterrupt, EOFError):
+ print("")
+ print("[Interrupted.]")
+ return
+ generate(d)
diff --git a/src/ablog/stylesheets/ablog/tagcloud.css b/src/ablog/stylesheets/ablog/tagcloud.css
new file mode 100644
index 0000000..7a6d07e
--- /dev/null
+++ b/src/ablog/stylesheets/ablog/tagcloud.css
@@ -0,0 +1,36 @@
+ul.ablog-cloud {
+ list-style: none;
+ overflow: auto;
+}
+
+ul.ablog-cloud li {
+ float: left;
+ height: 20pt;
+ line-height: 18pt;
+ margin-right: 5px;
+}
+
+ul.ablog-cloud a {
+ text-decoration: none;
+ vertical-align: middle;
+}
+
+li.ablog-cloud-1 {
+ font-size: 80%;
+}
+
+li.ablog-cloud-2 {
+ font-size: 95%;
+}
+
+li.ablog-cloud-3 {
+ font-size: 110%;
+}
+
+li.ablog-cloud-4 {
+ font-size: 125%;
+}
+
+li.ablog-cloud-5 {
+ font-size: 140%;
+}
diff --git a/src/ablog/templates/ablog/archives.html b/src/ablog/templates/ablog/archives.html
new file mode 100644
index 0000000..63d9c96
--- /dev/null
+++ b/src/ablog/templates/ablog/archives.html
@@ -0,0 +1,14 @@
+{% if ablog.archive %}
+<div class="ablog-sidebar-item ablog__archives">
+ <h3>
+ <a href="{{ pathto(ablog.archive.docname) }}">{{ gettext('Archives') }}</a>
+ </h3>
+ <ul>
+ {% for coll in ablog.archive %} {% if coll %}
+ <li>
+ <a href="{{ pathto(coll.docname) }}">{{ coll }} ({{ coll|length }})</a>
+ </li>
+ {% endif %} {% endfor %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/ablog/authors.html b/src/ablog/templates/ablog/authors.html
new file mode 100644
index 0000000..76b7368
--- /dev/null
+++ b/src/ablog/templates/ablog/authors.html
@@ -0,0 +1,14 @@
+{% if ablog.author %}
+<div class="ablog-sidebar-item ablog__authors">
+ <h3>
+ <a href="{{ pathto(ablog.author.path) }}">{{ gettext('Authors') }}</a>
+ </h3>
+ <ul>
+ {% for coll in ablog.author %} {% if coll %}
+ <li>
+ <a href="{{ pathto(coll.docname) }}">{{ coll }} ({{ coll|length }})</a>
+ </li>
+ {% endif %} {% endfor %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/ablog/catalog.html b/src/ablog/templates/ablog/catalog.html
new file mode 100644
index 0000000..5f746a0
--- /dev/null
+++ b/src/ablog/templates/ablog/catalog.html
@@ -0,0 +1,25 @@
+{%- extends "page.html" %} {% macro postlink(post) -%} {% if post.external_link
+-%} {{- post.external_link -}} {% else %} {{- pathto(post.docname) }}{{
+anchor(post) -}} {%- endif %} {%- endmacro %} {% block body %} {% for collection
+in catalog %} {% if collection %}
+<div class="section ablog__catalog_header">
+ <h2>
+ <span
+ >{{ header }}
+ <a href="{{ pathto(collection.docname) }}">{{ collection }}</a>
+ </span>
+ </h2>
+ {% for post in collection %}
+ <div class="section ablog__catalog_post">
+ <p>
+ {% if post.published %}
+ <span>{{ post.date.strftime(ablog.post_date_format) }}</span>
+ {% else %}
+ <span>Draft</span>
+ {% endif %} -
+ <a href="{{ postlink(post) }}">{{ post.title }}</a>
+ </p>
+ </div>
+ {% endfor %}
+</div>
+{% endif %} {% endfor %} {% endblock body %}
diff --git a/src/ablog/templates/ablog/categories.html b/src/ablog/templates/ablog/categories.html
new file mode 100644
index 0000000..38b67aa
--- /dev/null
+++ b/src/ablog/templates/ablog/categories.html
@@ -0,0 +1,14 @@
+{% if ablog.category %}
+<div class="ablog-sidebar-item ablog__categories">
+ <h3>
+ <a href="{{ pathto(ablog.category.path) }}">{{ gettext('Categories') }}</a>
+ </h3>
+ <ul>
+ {% for coll in ablog.category %} {% if coll %}
+ <li>
+ <a href="{{ pathto(coll.docname) }}">{{ coll }} ({{ coll|length }})</a>
+ </li>
+ {% endif %} {% endfor %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/ablog/collection.html b/src/ablog/templates/ablog/collection.html
new file mode 100644
index 0000000..6e1bf1a
--- /dev/null
+++ b/src/ablog/templates/ablog/collection.html
@@ -0,0 +1,57 @@
+{%- extends "page.html" %} {% block body %} {% macro postlink(post) -%} {% if
+post.external_link -%} {{- post.external_link -}} {% else %} {{-
+pathto(post.docname) }}{{ anchor(post) -}} {%- endif %} {%- endmacro %}
+<div class="section ablog__collection">
+ <h1>
+ {% if archive_feed and fa %}
+ <a href="{{ pathto(collection.path, 1) }}/atom.xml">
+ <i class="fa fa-rss fa-rotate-270"></i
+ ></a>
+ {% endif %}
+ <span
+ >{{ header }} {% if collection.href %}
+ <a href="{{ collection.href }}">{{ collection }}</a>
+ {% else %} {{ collection }} {% endif %}
+ </span>
+ </h1>
+ {% if ablog.blog_archive_titles %} {% for post in collection %}
+ <div class="section ablog__collection_meta">
+ <p>
+ {% if post.published %}
+ <span>{{ post.date.strftime(ablog.post_date_format) }}</span>
+ {% else %}
+ <span>Draft</span>
+ {% endif %} -
+ <a href="{{ postlink(post) }}">{{ post.title }}</a>
+ </p>
+ </div>
+ {% endfor %} {% else %} {% for post in collection %}
+ <div class="section ablog-post">
+ <h2 class="ablog-post-title">
+ <a href="{{ postlink(post) }}">{{ post.title }}</a>
+ </h2>
+ <ul class="ablog-archive">
+ <li>
+ {% if post.published %} {% if fa %}
+ <i class="fa fa-calendar"></i>
+ {% endif %}
+ <span>{{ post.date.strftime(ablog.post_date_format) }}</span>
+ {% else %} {% if fa %}
+ <i class="fa fa-pencil"></i>
+ {% endif %} {% if post.date %}
+ <span>{{ post.date.strftime(ablog.post_date_format) }}</span>
+ {% else %}
+ <span>Draft</span>
+ {% endif %} {% endif %}
+ </li>
+ {% include "ablog/postcard2.html" %}
+ </ul>
+ {{ post.to_html(collection.docname) }}
+ <p class="ablog-post-expand">
+ <a href="{{ postlink(post) }}"><em>{{ _("Read more ...") }}</em></a>
+ </p>
+ <hr />
+ </div>
+ {% endfor %} {% endif %}
+</div>
+{% endblock body %}
diff --git a/src/ablog/templates/ablog/languages.html b/src/ablog/templates/ablog/languages.html
new file mode 100644
index 0000000..69f6b0b
--- /dev/null
+++ b/src/ablog/templates/ablog/languages.html
@@ -0,0 +1,14 @@
+{% if ablog.language %}
+<div class="ablog-sidebar-item ablog__languages">
+ <h3>
+ <a href="{{ pathto(ablog.language.path) }}">{{ gettext('Languages') }}</a>
+ </h3>
+ <ul>
+ {% for coll in ablog.language %} {% if coll %}
+ <li>
+ <a href="{{ pathto(coll.docname) }}">{{ coll }} ({{ coll|length }})</a>
+ </li>
+ {% endif %} {% endfor %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/ablog/locations.html b/src/ablog/templates/ablog/locations.html
new file mode 100644
index 0000000..2ea6b05
--- /dev/null
+++ b/src/ablog/templates/ablog/locations.html
@@ -0,0 +1,14 @@
+{% if ablog.location %}
+<div class="ablog-sidebar-item ablog__locations">
+ <h3>
+ <a href="{{ pathto(ablog.location.path) }}">{{ gettext('Locations') }}</a>
+ </h3>
+ <ul>
+ {% for coll in ablog.location %} {% if coll %}
+ <li>
+ <a href="{{ pathto(coll.docname) }}">{{ coll }} ({{ coll|length }})</a>
+ </li>
+ {% endif %} {% endfor %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/ablog/postcard.html b/src/ablog/templates/ablog/postcard.html
new file mode 100644
index 0000000..cc8eb53
--- /dev/null
+++ b/src/ablog/templates/ablog/postcard.html
@@ -0,0 +1,21 @@
+{% if pagename in ablog %}
+<div class="ablog-sidebar-item ablog__postcard">
+ {% set fa = ablog.fontawesome %} {% set post = ablog[pagename] %}
+ <h2>
+ {% if post.published %} {% if fa %}
+ <i class="fa fa-calendar"></i>
+ {% endif %}
+ <span>{{ post.date.strftime(ablog.post_date_format) }}</span>
+ {% else %} {% if fa %}
+ <i class="fa fa-pencil"></i>
+ {% endif %} {% if post.date %}
+ <span>{{ post.date.strftime(ablog.post_date_format) }}</span>
+ {% else %}
+ <span>Draft</span>
+ {% endif %} {% endif %}
+ </h2>
+ <ul>
+ {% include "ablog/postcard2.html" %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/ablog/postcard2.html b/src/ablog/templates/ablog/postcard2.html
new file mode 100644
index 0000000..16a8337
--- /dev/null
+++ b/src/ablog/templates/ablog/postcard2.html
@@ -0,0 +1,105 @@
+<div class="ablog-sidebar-item ablog__postcard2">
+ {% if post.published and post.date != post.update %}
+ <li id="published">
+ <span>
+ {% if fa %}
+ <i class="fa fa-fw fa-edit"></i>
+ {% else %} {{ gettext('Update') }}: {% endif %}
+ </span>
+ {{ post.update.strftime(ablog.post_date_format) }}
+ </li>
+ {% endif %} {% if post.author %}
+ <li id="ablog-sidebar-item author ablog__author">
+ <span>
+ {% if fa %}
+ <i class="fa-fw fa fa-user"></i>
+ {% else %} {{ gettext('Author') }}: {% endif %}
+ </span>
+ {% for coll in post.author %} {% if coll|length %}
+ <a href="{{ pathto(coll.docname) }}">{{ coll }}</a>
+ {% if loop.index < post.author|length %} , {% endif %} {% else %} {{ coll }}
+ {% if loop.index < post.author|length %} , {% endif %} {% endif %} {% endfor
+ %}
+ </li>
+ {% endif %} {% if post.location %}
+ <li id="ablog-sidebar-item location ablog__location">
+ <span>
+ {% if fa %}
+ <i class="fa-fw fa fa-location-arrow"></i>
+ {% else %} {{ gettext('Location') }}: {% endif %}
+ </span>
+ {% for coll in post.location %} {% if coll|length %}
+ <a href="{{ pathto(coll.docname) }}">{{ coll }}</a>
+ {% if loop.index < post.location|length %} , {% endif %} {% else %} {{ coll
+ }} {% if loop.index < post.location|length %} , {% endif %} {% endif %} {%
+ endfor %}
+ </li>
+ {% endif %} {% if post.language %}
+ <li id="ablog-sidebar-item language ablog__language">
+ <span>
+ {% if fa %}
+ <i class="fa-fw fa fa-language"></i>
+ {% else %} {{ gettext('Language') }}: {% endif %}
+ </span>
+ {% for coll in post.language %} {% if coll|length %}
+ <a href="{{ pathto(coll.docname) }}">{{ coll }}</a>
+ {% if loop.index < post.language|length %} , {% endif %} {% else %} {{ coll
+ }} {% if loop.index < post.language|length %} , {% endif %} {% endif %} {%
+ endfor %}
+ </li>
+ {% endif %} {% if post.category %}
+ <li id="ablog-sidebar-item category ablog__category">
+ <span>
+ {% if fa %}
+ <i class="fa-fw fa fa-folder-open"></i>
+ {% else %} {{ gettext('Category') }}: {% endif %}
+ </span>
+ {% for coll in post.category %} {% if coll|length %}
+ <a href="{{ pathto(coll.docname) }}">{{ coll }}</a>
+ {% if loop.index < post.category|length %} , {% endif %} {% else %} {{ coll
+ }} {% if loop.index < post.category|length %} , {% endif %} {% endif %} {%
+ endfor %}
+ </li>
+ {% endif %} {% if post.tags %}
+ <li id="ablog-sidebar-item tags ablog__tags">
+ <span>
+ {% if post.tags|length > 1 %} {% if fa %}
+ <i class="fa-fw fa fa-tags"></i>
+ {% else %} {{ gettext('Tags') }}: {% endif %} {% else %} {% if fa %}
+ <i class="fa-fw fa fa-tag"></i>
+ {% else %} {{ gettext('Tag') }}: {% endif %}{% endif %}
+ </span>
+ {% for coll in post.tags %} {% if coll|length %}
+ <a href="{{ pathto(coll.docname) }}">{{ coll }}</a>
+ {% if loop.index < post.tags|length %} {% endif %} {% else %} {{ coll }} {%
+ if loop.index < post.tags|length %} {% endif %} {% endif %} {% endfor %}
+ </li>
+ {% endif %} {% if ablog.disqus_shortname and (ablog[pagename].published or
+ ablog.disqus_drafts) %}
+ <li id="ablog-sidebar-item comments ablog__comments">
+ <script type="text/javascript">
+ var disqus_shortname = "{{ ablog.disqus_shortname }}";
+
+ (function () {
+ var s = document.createElement("script");
+ s.async = true;
+ s.type = "text/javascript";
+ s.src = "//" + disqus_shortname + ".disqus.com/count.js";
+ (
+ document.getElementsByTagName("HEAD")[0] ||
+ document.getElementsByTagName("BODY")[0]
+ ).appendChild(s);
+ })();
+ </script>
+ {% if fa %}
+ <i class="fa-fw fa fa-comments"></i>
+ {% endif %}
+ <a
+ href="{%- if pagename != post.docname -%} {{ pathto(post.docname) }} {%- endif -%} #disqus_thread"
+ data-disqus-identifier="/{{ post.docname }}/"
+ >
+ {% if not fa %} Comments {% endif %}
+ </a>
+ </li>
+ {% endif %}
+</div>
diff --git a/src/ablog/templates/ablog/postnavy.html b/src/ablog/templates/ablog/postnavy.html
new file mode 100644
index 0000000..6cb11c1
--- /dev/null
+++ b/src/ablog/templates/ablog/postnavy.html
@@ -0,0 +1,28 @@
+{# prev/next are not set for drafts #} {% set post = ablog[pagename] %} {% if
+post.published and ablog.post_show_prev_next %}
+<div class="section ablog__prev-next">
+ <span class="ablog__prev">
+ {% if post.prev %} {% if not ablog.fontawesome %} {{ gettext('Previous') }}:
+ {% endif %}
+ <a href="{{ pathto(post.prev.docname) }}{{ anchor(post.prev) }}">
+ {% if ablog.fontawesome %}
+ <i class="fa fa-arrow-circle-left"></i>
+ {% endif %}
+ <span>{{ post.prev.title }}</span>
+ </a>
+ {% endif %}
+ </span>
+ <span class="ablog__spacer">&nbsp;</span>
+ <span class="ablog__next">
+ {% if post.next %} {% if not ablog.fontawesome %} {{ gettext('Next') }}: {%
+ endif %}
+ <a href="{{ pathto(post.next.docname) }}{{ anchor(post.next) }}">
+ <span>{{ post.next.title }}</span>
+ {% if ablog.fontawesome %}
+ <i class="fa fa-arrow-circle-right"></i>
+ {% endif %}
+ </a>
+ {% endif %}
+ </span>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/ablog/recentposts.html b/src/ablog/templates/ablog/recentposts.html
new file mode 100644
index 0000000..91953bf
--- /dev/null
+++ b/src/ablog/templates/ablog/recentposts.html
@@ -0,0 +1,17 @@
+{% if ablog %}
+<div class="ablog-sidebar-item ablog__recentposts">
+ <h3>
+ <a href="{{ pathto(ablog.blog_path) }}">{{ gettext('Recent Posts') }}</a>
+ </h3>
+ <ul>
+ {% set pcount = 1 %} {% for recent in ablog.recent(5, pagename) %}
+ <li>
+ <a href="{{ pathto(recent.docname) }}{{ anchor(recent) }}">
+ {{ recent.date.strftime(ablog.post_date_format_short) + " - " +
+ recent.title }}
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/ablog/redirect.html b/src/ablog/templates/ablog/redirect.html
new file mode 100644
index 0000000..e2ce792
--- /dev/null
+++ b/src/ablog/templates/ablog/redirect.html
@@ -0,0 +1,8 @@
+{%- extends "!layout.html" %} {%- block extrahead %} {{ super() }}
+<meta
+ http-equiv="refresh"
+ content="{{ ablog.post_redirect_refresh }}; url={{ pathto(redirect) }}"
+/>
+{% endblock extrahead %} {% block body %} You are being redirected to
+<a href="{{ pathto(redirect) }}">{{ post.title }}</a> in {{
+ablog.post_redirect_refresh }} seconds; {% endblock body %}
diff --git a/src/ablog/templates/ablog/tagcloud.html b/src/ablog/templates/ablog/tagcloud.html
new file mode 100644
index 0000000..278a62b
--- /dev/null
+++ b/src/ablog/templates/ablog/tagcloud.html
@@ -0,0 +1,17 @@
+{% if ablog.tags %}
+<div class="ablog-sidebar-item ablog__tagcloud">
+ <link
+ rel="stylesheet"
+ href="{{ pathto('_static/ablog/tagcloud.css', 1) }}"
+ type="text/css"
+ />
+ <h3><a href="{{ pathto(ablog.tags.path) }}">{{ gettext('Tags') }}</a></h3>
+ <ul class="ablog-cloud">
+ {% for coll in ablog.tags %} {% if coll %}
+ <li class="ablog-cloud ablog-cloud-{{ coll.relsize(5, 1) }}">
+ <a href="{{ pathto(coll.docname) }}">{{ coll }}</a>
+ </li>
+ {% endif %} {% endfor %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/archives.html b/src/ablog/templates/archives.html
new file mode 100644
index 0000000..9a10cdf
--- /dev/null
+++ b/src/ablog/templates/archives.html
@@ -0,0 +1,15 @@
+{{ warning("archives.html is an old template path, that is no longer used by
+ablog. Please use ablog/archives.html instead.") }} {% if ablog.archive %}
+<div class="ablog-sidebar-item ablog__archive">
+ <h3>
+ <a href="{{ pathto(ablog.archive.docname) }}">{{ gettext('Archives') }}</a>
+ </h3>
+ <ul>
+ {% for coll in ablog.archive %} {% if coll %}
+ <li>
+ <a href="{{ pathto(coll.docname) }}">{{ coll }} ({{ coll|length }})</a>
+ </li>
+ {% endif %} {% endfor %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/authors.html b/src/ablog/templates/authors.html
new file mode 100644
index 0000000..28bc96c
--- /dev/null
+++ b/src/ablog/templates/authors.html
@@ -0,0 +1,15 @@
+{{ warning("authors.html is an old template path, that is no longer used by
+ablog. Please use ablog/authors.html instead.") }} {% if ablog.author %}
+<div class="ablog-sidebar-item ablog__authors">
+ <h3>
+ <a href="{{ pathto(ablog.author.path) }}">{{ gettext('Authors') }}</a>
+ </h3>
+ <ul>
+ {% for coll in ablog.author %} {% if coll %}
+ <li>
+ <a href="{{ pathto(coll.docname) }}">{{ coll }} ({{ coll|length }})</a>
+ </li>
+ {% endif %} {% endfor %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/catalog.html b/src/ablog/templates/catalog.html
new file mode 100644
index 0000000..196a851
--- /dev/null
+++ b/src/ablog/templates/catalog.html
@@ -0,0 +1,26 @@
+{{ warning("catalog.html is an old template path, that is no longer used by
+ablog. Please use ablog/catalog.html instead.") }} {%- extends "page.html" %} {%
+macro postlink(post) -%} {% if post.external_link -%} {{- post.external_link -}}
+{% else %} {{- pathto(post.docname) }}{{ anchor(post) -}} {%- endif %} {%-
+endmacro %} {% block body %} {% for collection in catalog %} {% if collection %}
+<div class="section ablog__catalog_header">
+ <h2>
+ <span
+ >{{ header }}
+ <a href="{{ pathto(collection.docname) }}">{{ collection }}</a></span
+ >
+ </h2>
+ {% for post in collection %}
+ <div class="section ablog__catalog_post">
+ <p>
+ {% if post.published %}
+ <span>{{ post.date.strftime(ablog.post_date_format) }}</span>
+ {% else %}
+ <span>Draft</span>
+ {% endif %} -
+ <a href="{{ postlink(post) }}">{{ post.title }}</a>
+ </p>
+ </div>
+ {% endfor %}
+</div>
+{% endif %} {% endfor %} {% endblock body %}
diff --git a/src/ablog/templates/categories.html b/src/ablog/templates/categories.html
new file mode 100644
index 0000000..cbf92d9
--- /dev/null
+++ b/src/ablog/templates/categories.html
@@ -0,0 +1,15 @@
+{{ warning("category.html is an old template path, that is no longer used by
+ablog. Please use ablog/category.html instead.") }} {% if ablog.category %}
+<div class="ablog-sidebar-item ablog__category">
+ <h3>
+ <a href="{{ pathto(ablog.category.path) }}">{{ gettext('Categories') }}</a>
+ </h3>
+ <ul>
+ {% for coll in ablog.category %} {% if coll %}
+ <li>
+ <a href="{{ pathto(coll.docname) }}">{{ coll }} ({{ coll|length }})</a>
+ </li>
+ {% endif %} {% endfor %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/collection.html b/src/ablog/templates/collection.html
new file mode 100644
index 0000000..93b668c
--- /dev/null
+++ b/src/ablog/templates/collection.html
@@ -0,0 +1,59 @@
+{{ warning("collection.html is an old template path, that is no longer used by
+ablog. Please use ablog/collection.html instead.") }} {%- extends "page.html" %}
+{% block body %} {% macro postlink(post) -%} {% if post.external_link -%} {{-
+post.external_link -}} {% else %} {{- pathto(post.docname) }}{{ anchor(post) -}}
+{%- endif %} {%- endmacro %} {% endmacro %}
+<div class="section ablog__collection">
+ <h1>
+ {% if archive_feed and fa %}
+ <a href="{{ pathto(collection.path, 1) }}/atom.xml">
+ <i class="fa fa-rss fa-rotate-270"></i
+ ></a>
+ {% endif %}
+ <span
+ >{{ header }} {% if collection.href %}
+ <a href="{{ collection.href }}">{{ collection }}</a>
+ {% else %} {{ collection }} {% endif %}
+ </span>
+ </h1>
+ {% if ablog.blog_archive_titles %} {% for post in collection %}
+ <div class="section ablog__collection_meta">
+ <p>
+ {% if post.published %}
+ <span>{{ post.date.strftime(ablog.post_date_format) }}</span>
+ {% else %}
+ <span>Draft</span>
+ {% endif %} -
+ <a href="{{ postlink(post) }}">{{ post.title }}</a>
+ </p>
+ </div>
+ {% endfor %} {% else %} {% for post in collection %}
+ <div class="section ablog-post">
+ <h2 class="ablog-post-title">
+ <a href="{{ postlink(post) }}">{{ post.title }}</a>
+ </h2>
+ <ul class="ablog-archive">
+ <li>
+ {% if post.published %} {% if fa %}
+ <i class="fa fa-calendar"></i>
+ {% endif %}
+ <span>{{ post.date.strftime(ablog.post_date_format) }}</span>
+ {% else %} {% if fa %}
+ <i class="fa fa-pencil"></i>
+ {% endif %} {% if post.date %}
+ <span>{{ post.date.strftime(ablog.post_date_format) }}</span>
+ {% else %}
+ <span>Draft</span>
+ {% endif %} {% endif %}
+ </li>
+ {% include "ablog/postcard2.html" %}
+ </ul>
+ {{ post.to_html(collection.docname) }}
+ <p class="ablog-post-expand">
+ <a href="{{ postlink(post) }}"><em>{{ _("Read more ...") }}</em></a>
+ </p>
+ <hr />
+ </div>
+ {% endfor %} {% endif %}
+</div>
+{% endblock body %}
diff --git a/src/ablog/templates/languages.html b/src/ablog/templates/languages.html
new file mode 100644
index 0000000..7353396
--- /dev/null
+++ b/src/ablog/templates/languages.html
@@ -0,0 +1,15 @@
+{{ warning("languages.html is an old template path, that is no longer used by
+ablog. Please use ablog/languages.html instead.") }} {% if ablog.language %}
+<div class="ablog-sidebar-item ablog__languages">
+ <h3>
+ <a href="{{ pathto(ablog.language.path) }}">{{ gettext('Languages') }}</a>
+ </h3>
+ <ul>
+ {% for coll in ablog.language %} {% if coll %}
+ <li>
+ <a href="{{ pathto(coll.docname) }}">{{ coll }} ({{ coll|length }})</a>
+ </li>
+ {% endif %} {% endfor %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/locations.html b/src/ablog/templates/locations.html
new file mode 100644
index 0000000..27600f3
--- /dev/null
+++ b/src/ablog/templates/locations.html
@@ -0,0 +1,15 @@
+{{ warning("locations.html is an old template path, that is no longer used by
+ablog. Please use ablog/locations.html instead.") }} {% if ablog.location %}
+<div class="ablog-sidebar-item ablog__locations">
+ <h3>
+ <a href="{{ pathto(ablog.location.path) }}">{{ gettext('Locations') }}</a>
+ </h3>
+ <ul>
+ {% for coll in ablog.location %} {% if coll %}
+ <li>
+ <a href="{{ pathto(coll.docname) }}">{{ coll }} ({{ coll|length }})</a>
+ </li>
+ {% endif %} {% endfor %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/page.html b/src/ablog/templates/page.html
new file mode 100644
index 0000000..5ec0ed3
--- /dev/null
+++ b/src/ablog/templates/page.html
@@ -0,0 +1,56 @@
+{%- extends "layout.html" %} {% set fa = ablog.fontawesome %} {%- block
+extrahead %} {{ super() }} {% if feed_path %}
+<link
+ rel="alternate"
+ type="application/atom+xml"
+ href="{{ pathto(feed_path, 1) }}/atom.xml"
+ title="{{ feed_title }}"
+/>
+{% endif %} {% if ablog.fontawesome_link_cdn %}
+<link href="{{ ablog.fontawesome_link_cdn }}" rel="stylesheet" />
+{% elif ablog.fontawesome_css_file %}
+<link
+ rel="stylesheet"
+ href="{{ pathto('_static/' + ablog.fontawesome_css_file, 1) }}"
+ type="text/css"
+/>
+{% endif %} {% endblock extrahead %} {% block body %} {{ body }}
+<div class="section ablog__blog_comments">
+ {% if pagename in ablog %} {% include "ablog/postnavy.html" %} {% endif %} {%
+ if ablog.disqus_shortname and ablog.blog_baseurl and (not
+ ablog[pagename].nocomments) and ((pagename in ablog and
+ (ablog[pagename].published or ablog.disqus_drafts)) or (not pagename in ablog
+ and ablog.disqus_pages)) %}
+ <div class="section ablog__comments">
+ <h2>Comments</h2>
+ <div id="disqus_thread"></div>
+ <script type="text/javascript">
+ var disqus_shortname = "{{ ablog.disqus_shortname }}";
+ var disqus_identifier = "{{ablog.page_id(pagename)}}";
+ var disqus_title = "{{title|e}}";
+ var disqus_url = "{{ablog.page_url(pagename)}}";
+
+ (function () {
+ var dsq = document.createElement("script");
+ dsq.type = "text/javascript";
+ dsq.async = true;
+ dsq.src = "//" + disqus_shortname + ".disqus.com/embed.js";
+ (
+ document.getElementsByTagName("head")[0] ||
+ document.getElementsByTagName("body")[0]
+ ).appendChild(dsq);
+ })();
+ </script>
+ <noscript>
+ Please enable JavaScript to view the
+ <a href="https://disqus.com/?ref_noscript">
+ comments powered by Disqus.</a
+ ></noscript
+ >
+ <a href="https://disqus.com" class="dsq-brlink">
+ comments powered by <span class="logo-disqus">Disqus</span>
+ </a>
+ </div>
+ {% endif %}
+</div>
+{% endblock body %}
diff --git a/src/ablog/templates/postcard.html b/src/ablog/templates/postcard.html
new file mode 100644
index 0000000..5e82b1c
--- /dev/null
+++ b/src/ablog/templates/postcard.html
@@ -0,0 +1,22 @@
+{{ warning("postcard.html is an old template path, that is no longer used by
+ablog. Please use ablog/postcard.html instead.") }} {% if pagename in ablog %}
+<div class="ablog-sidebar-item ablog__postcard">
+ {% set fa = ablog.fontawesome %} {% set post = ablog[pagename] %}
+ <h2>
+ {% if post.published %} {% if fa %}
+ <i class="fa fa-calendar"></i>
+ {% endif %}
+ <span>{{ post.date.strftime(ablog.post_date_format) }}</span>
+ {% else %} {% if fa %}
+ <i class="fa fa-pencil"></i>
+ {% endif %} {% if post.date %}
+ <span>{{ post.date.strftime(ablog.post_date_format) }}</span>
+ {% else %}
+ <span>Draft</span>
+ {% endif %} {% endif %}
+ </h2>
+ <ul>
+ {% include "ablog/postcard2.html" %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/postcard2.html b/src/ablog/templates/postcard2.html
new file mode 100644
index 0000000..5dbdfb3
--- /dev/null
+++ b/src/ablog/templates/postcard2.html
@@ -0,0 +1,107 @@
+{{ warning("postcard2.html is an old template path, that is no longer used by
+ablog. Please use ablog/postcard2.html instead.") }}
+<div class="ablog-sidebar-item ablog__postcard2">
+ {% if post.published and post.date != post.update %}
+ <li id="published">
+ <span>
+ {% if fa %}
+ <i class="fa fa-fw fa-edit"></i>
+ {% else %} {{ gettext('Update') }}: {% endif %}
+ </span>
+ {{ post.update.strftime(ablog.post_date_format) }}
+ </li>
+ {% endif %} {% if post.author %}
+ <li id="ablog-sidebar-item author ablog__author">
+ <span>
+ {% if fa %}
+ <i class="fa-fw fa fa-user"></i>
+ {% else %} {{ gettext('Author') }}: {% endif %}
+ </span>
+ {% for coll in post.author %} {% if coll|length %}
+ <a href="{{ pathto(coll.docname) }}">{{ coll }}</a>
+ {% if loop.index < post.author|length %} , {% endif %} {% else %} {{ coll }}
+ {% if loop.index < post.author|length %} , {% endif %} {% endif %} {% endfor
+ %}
+ </li>
+ {% endif %} {% if post.location %}
+ <li id="ablog-sidebar-item location ablog__location">
+ <span>
+ {% if fa %}
+ <i class="fa-fw fa fa-location-arrow"></i>
+ {% else %} {{ gettext('Location') }}: {% endif %}
+ </span>
+ {% for coll in post.location %} {% if coll|length %}
+ <a href="{{ pathto(coll.docname) }}">{{ coll }}</a>
+ {% if loop.index < post.location|length %} , {% endif %} {% else %} {{ coll
+ }} {% if loop.index < post.location|length %} , {% endif %} {% endif %} {%
+ endfor %}
+ </li>
+ {% endif %} {% if post.language %}
+ <li id="ablog-sidebar-item language ablog__language">
+ <span>
+ {% if fa %}
+ <i class="fa-fw fa fa-language"></i>
+ {% else %} {{ gettext('Language') }}: {% endif %}
+ </span>
+ {% for coll in post.language %} {% if coll|length %}
+ <a href="{{ pathto(coll.docname) }}">{{ coll }}</a>
+ {% if loop.index < post.language|length %} , {% endif %} {% else %} {{ coll
+ }} {% if loop.index < post.language|length %} , {% endif %} {% endif %} {%
+ endfor %}
+ </li>
+ {% endif %} {% if post.category %}
+ <li id="ablog-sidebar-item category ablog__category">
+ <span>
+ {% if fa %}
+ <i class="fa-fw fa fa-folder-open"></i>
+ {% else %} {{ gettext('Category') }}: {% endif %}
+ </span>
+ {% for coll in post.category %} {% if coll|length %}
+ <a href="{{ pathto(coll.docname) }}">{{ coll }}</a>
+ {% if loop.index < post.category|length %} , {% endif %} {% else %} {{ coll
+ }} {% if loop.index < post.category|length %} , {% endif %} {% endif %} {%
+ endfor %}
+ </li>
+ {% endif %} {% if post.tags %}
+ <li id="ablog-sidebar-item tags ablog__tags">
+ <span>
+ {% if post.tags|length > 1 %} {% if fa %}
+ <i class="fa-fw fa fa-tags"></i>
+ {% else %} {{ gettext('Tags') }}: {% endif %} {% else %} {% if fa %}
+ <i class="fa-fw fa fa-tag"></i>
+ {% else %} {{ gettext('Tag') }}: {% endif %}{% endif %}
+ </span>
+ {% for coll in post.tags %} {% if coll|length %}
+ <a href="{{ pathto(coll.docname) }}">{{ coll }}</a>
+ {% if loop.index < post.tags|length %} {% endif %} {% else %} {{ coll }} {%
+ if loop.index < post.tags|length %} {% endif %} {% endif %} {% endfor %}
+ </li>
+ {% endif %} {% if ablog.disqus_shortname and (ablog[pagename].published or
+ ablog.disqus_drafts) %}
+ <li id="ablog-sidebar-item comments ablog__comments">
+ <script type="text/javascript">
+ var disqus_shortname = "{{ ablog.disqus_shortname }}";
+
+ (function () {
+ var s = document.createElement("script");
+ s.async = true;
+ s.type = "text/javascript";
+ s.src = "//" + disqus_shortname + ".disqus.com/count.js";
+ (
+ document.getElementsByTagName("HEAD")[0] ||
+ document.getElementsByTagName("BODY")[0]
+ ).appendChild(s);
+ })();
+ </script>
+ {% if fa %}
+ <i class="fa-fw fa fa-comments"></i>
+ {% endif %}
+ <a
+ href="{%- if pagename != post.docname -%} {{ pathto(post.docname) }} {%- endif -%} #disqus_thread"
+ data-disqus-identifier="/{{ post.docname }}/"
+ >
+ {% if not fa %} Comments {% endif %}
+ </a>
+ </li>
+ {% endif %}
+</div>
diff --git a/src/ablog/templates/postnavy.html b/src/ablog/templates/postnavy.html
new file mode 100644
index 0000000..099de14
--- /dev/null
+++ b/src/ablog/templates/postnavy.html
@@ -0,0 +1,30 @@
+{{ warning("postnavy.html is an old template path, that is no longer used by
+ablog. Please use ablog/postnavy.html instead.") }} {# prev/next are not set for
+drafts #} {% set post = ablog[pagename] %} {% if post.published and
+ablog.post_show_prev_next %}
+<div class="section ablog__prev-next">
+ <span>
+ {% if post.prev %} {% if not ablog.fontawesome %} {{ gettext('Previous') }}:
+ {% endif %}
+ <a href="{{ pathto(post.prev.docname) }}{{ anchor(post.prev) }}">
+ {% if ablog.fontawesome %}
+ <i class="fa fa-arrow-circle-left"></i>
+ {% endif %}
+ <span>{{ post.prev.title }}</span>
+ </a>
+ {% endif %}
+ </span>
+ <span>&nbsp;</span>
+ <span>
+ {% if post.next %} {% if not ablog.fontawesome %} {{ gettext('Next') }}: {%
+ endif %}
+ <a href="{{ pathto(post.next.docname) }}{{ anchor(post.next) }}">
+ <span>{{ post.next.title }}</span>
+ {% if ablog.fontawesome %}
+ <i class="fa fa-arrow-circle-right"></i>
+ {% endif %}
+ </a>
+ {% endif %}
+ </span>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/recentposts.html b/src/ablog/templates/recentposts.html
new file mode 100644
index 0000000..7a8860c
--- /dev/null
+++ b/src/ablog/templates/recentposts.html
@@ -0,0 +1,18 @@
+{{ warning("recentposts.html is an old template path, that is no longer used by
+ablog. Please use ablog/recentposts.html instead.") }} {% if ablog %}
+<div class="ablog-sidebar-item ablog__recentposts">
+ <h3>
+ <a href="{{ pathto(ablog.blog_path) }}">{{ gettext('Recent Posts') }}</a>
+ </h3>
+ <ul>
+ {% set pcount = 1 %} {% for recent in ablog.recent(5, pagename) %}
+ <li>
+ <a href="{{ pathto(recent.docname) }}{{ anchor(recent) }}">
+ {{ recent.date.strftime(ablog.post_date_format_short) + " - " +
+ recent.title }}
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/templates/redirect.html b/src/ablog/templates/redirect.html
new file mode 100644
index 0000000..1230494
--- /dev/null
+++ b/src/ablog/templates/redirect.html
@@ -0,0 +1,10 @@
+{{ warning("redirect.html is an old template path, that is no longer used by
+ablog. Please use ablog/redirect.html instead.") }} {%- extends "!layout.html"
+%} {%- block extrahead %} {{ super() }}
+<meta
+ http-equiv="refresh"
+ content="{{ ablog.post_redirect_refresh }}; url={{ pathto(redirect) }}"
+/>
+{% endblock extrahead %} {% block body %} You are being redirected to
+<a href="{{ pathto(redirect) }}">{{ post.title }}</a> in {{
+ablog.post_redirect_refresh }} seconds; {% endblock body %}
diff --git a/src/ablog/templates/tagcloud.html b/src/ablog/templates/tagcloud.html
new file mode 100644
index 0000000..f317afe
--- /dev/null
+++ b/src/ablog/templates/tagcloud.html
@@ -0,0 +1,18 @@
+{{ warning("tagcloud.html is an old template path, that is no longer used by
+ablog. Please use ablog/tagcloud.html instead.") }} {% if ablog.tags %}
+<div class="ablog-sidebar-item ablog__tags">
+ <link
+ rel="stylesheet"
+ href="{{ pathto('_static/ablog/tagcloud.css', 1) }}"
+ type="text/css"
+ />
+ <h3><a href="{{ pathto(ablog.tags.path) }}">{{ gettext('Tags') }}</a></h3>
+ <ul class="ablog-cloud">
+ {% for coll in ablog.tags %} {% if coll %}
+ <li class="ablog-cloud ablog-cloud-{{ coll.relsize(5, 1) }}">
+ <a href="{{ pathto(coll.docname) }}">{{ coll }}</a>
+ </li>
+ {% endif %} {% endfor %}
+ </ul>
+</div>
+{% endif %}
diff --git a/src/ablog/tests/__init__.py b/src/ablog/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/ablog/tests/__init__.py
diff --git a/src/ablog/tests/test_build.py b/src/ablog/tests/test_build.py
new file mode 100644
index 0000000..e5951bd
--- /dev/null
+++ b/src/ablog/tests/test_build.py
@@ -0,0 +1,84 @@
+from datetime import datetime
+
+import lxml
+import pytest
+
+POST_DATETIME_FMT = "%Y-%m-%dT%H:%M:%S%z"
+
+
+@pytest.mark.sphinx("html", testroot="build") # using roots/test-build
+def test_build(app, status, warning):
+ app.build()
+
+ assert app.statuscode == 0
+ assert (app.outdir / "index.html").exists()
+ assert (app.outdir / "blog/archive.html").exists()
+ assert (app.outdir / "post.html").exists()
+
+
+@pytest.mark.sphinx("html", testroot="build") # using roots/test-build
+def test_feed(app, status, warning):
+ """
+ Atom syndication feeds are built correctly.
+ """
+ app.build()
+ assert app.statuscode == 0
+
+ feed_path = app.outdir / "blog/atom.xml"
+ assert (feed_path).exists()
+
+ with feed_path.open() as feed_opened:
+ feed_tree = lxml.etree.parse(feed_opened)
+ entries = feed_tree.findall("{http://www.w3.org/2005/Atom}entry")
+ assert len(entries) == 2
+
+ entry = entries[0]
+ title = entry.find("{http://www.w3.org/2005/Atom}title")
+ assert title.text == "Foo Post Title"
+ summary = entry.find("{http://www.w3.org/2005/Atom}summary")
+ assert summary.text == "Foo post description with link."
+ categories = entry.findall("{http://www.w3.org/2005/Atom}category")
+ assert len(categories) == 2
+ assert categories[0].attrib["label"] == "BarTag"
+ assert categories[0].attrib["term"] == "BarTag"
+ assert categories[1].attrib["label"] == "Foo Tag"
+ assert categories[1].attrib["term"] == "FooTag"
+ content = entry.find("{http://www.w3.org/2005/Atom}content")
+ assert "Foo post content." in content.text
+ update_time = entry.find("{http://www.w3.org/2005/Atom}updated")
+ first_entry_date = datetime.strptime(update_time.text, POST_DATETIME_FMT)
+
+ empty_entry = entries[1]
+ title = empty_entry.find("{http://www.w3.org/2005/Atom}title")
+ assert title.text == "Foo Empty Post"
+ summary = empty_entry.find("{http://www.w3.org/2005/Atom}summary")
+ assert summary is None
+ categories = empty_entry.findall("{http://www.w3.org/2005/Atom}category")
+ assert len(categories) == 0
+ content = empty_entry.find("{http://www.w3.org/2005/Atom}content")
+ assert 'id="foo-empty-post"' in content.text
+ update_time = empty_entry.find("{http://www.w3.org/2005/Atom}updated")
+ second_entry_date = datetime.strptime(update_time.text, POST_DATETIME_FMT)
+
+ # check order of post based on their dates
+ assert first_entry_date > second_entry_date
+
+ social_path = app.outdir / "blog/social.xml"
+ assert (social_path).exists()
+
+ with social_path.open() as social_opened:
+ social_tree = lxml.etree.parse(social_opened)
+ social_entries = social_tree.findall("{http://www.w3.org/2005/Atom}entry")
+ assert len(social_entries) == len(entries)
+
+ social_entry = social_entries[0]
+ title = social_entry.find("{http://www.w3.org/2005/Atom}title")
+ assert title.text == "Foo Post Title"
+ summary = social_entry.find("{http://www.w3.org/2005/Atom}summary")
+ assert summary.text == "Foo post description with link."
+ categories = social_entry.findall("{http://www.w3.org/2005/Atom}category")
+ assert len(categories) == 2
+ assert categories[0].attrib["label"] == "BarTag"
+ assert categories[1].attrib["label"] == "Foo Tag"
+ content = social_entry.find("{http://www.w3.org/2005/Atom}content")
+ assert "Foo Post Title" in content.text
diff --git a/src/ablog/tests/test_canonical.py b/src/ablog/tests/test_canonical.py
new file mode 100644
index 0000000..1234645
--- /dev/null
+++ b/src/ablog/tests/test_canonical.py
@@ -0,0 +1,25 @@
+import pytest
+
+
+def read_text(path):
+ """
+ Support function to give backward compatibility with older sphinx (v2).
+ """
+ if hasattr(path, "read_text"):
+ return path.read_text()
+ return path.text()
+
+
+@pytest.mark.sphinx("html", testroot="canonical") # using roots/test-canonical
+def test_canonical(app, status, warning):
+ app.build()
+
+ assert app.statuscode == 0
+ assert (app.outdir / "post.html").exists()
+ assert (app.outdir / "canonical.html").exists()
+
+ html = read_text(app.outdir / "post.html")
+ assert '<link rel="canonical" href="https://blog.example.com/post.html" />' in html
+
+ html = read_text(app.outdir / "canonical.html")
+ assert '<link rel="canonical" href="https://canonical.example.org/foo.html" />' in html
diff --git a/src/ablog/tests/test_external.py b/src/ablog/tests/test_external.py
new file mode 100644
index 0000000..dbffded
--- /dev/null
+++ b/src/ablog/tests/test_external.py
@@ -0,0 +1,32 @@
+import pytest
+
+
+def read_text(path):
+ """
+ Support function to give backward compatibility with older sphinx (v2).
+ """
+ if hasattr(path, "read_text"):
+ return path.read_text()
+ return path.text()
+
+
+@pytest.mark.sphinx("html", testroot="external") # using roots/test-external
+def test_external(app, status, warning):
+ app.build()
+
+ assert app.statuscode == 0
+ assert (app.outdir / "external.html").exists()
+ assert (app.outdir / "postlist.html").exists()
+
+ html = read_text(app.outdir / "external.html")
+ url = "https://www.sphinx-doc.org/en/master/"
+ text = "This text will be in auto-generated post previews"
+ # The page itself lacks the URL
+ assert url not in html
+ # It does have the text we added
+ assert text in html
+
+ html = read_text(app.outdir / "postlist.html")
+ assert (
+ '<a class="reference external" href="https://www.sphinx-doc.org/en/master/">External post</a></p></li>' in html
+ )
diff --git a/src/ablog/tests/test_parallel.py b/src/ablog/tests/test_parallel.py
new file mode 100644
index 0000000..ddb9f6e
--- /dev/null
+++ b/src/ablog/tests/test_parallel.py
@@ -0,0 +1,34 @@
+from pathlib import Path
+from subprocess import run
+import sys
+import pytest
+
+
+@pytest.mark.xfail("win" in sys.platform, reason="Passes on Windows")
+def test_not_safe_for_parallel_read(rootdir: Path, tmp_path: Path):
+ """
+ Ablog is NOT safe for parallel read.
+
+ In such case, it doesn't collect any posts.
+ """
+ # https://github.com/sunpy/ablog/issues/297
+ # Very ugly hack to change the parallel_read_safe value to True
+ good_read_safe = '"parallel_read_safe": False'
+ bad_read_safe = '"parallel_read_safe": True'
+ init_py_path = Path(__file__).parent.parent / "__init__.py"
+ assert good_read_safe in init_py_path.read_text(encoding="utf-8")
+ bad_init_py = init_py_path.read_text().replace(good_read_safe, bad_read_safe)
+ init_py_path.write_text(bad_init_py, encoding="utf-8")
+
+ # liborjelinek: I wasn't able to demonstrate the issue with the `parallel` argument to the `sphinx` fixture
+ # @pytest.mark.sphinx("html", testroot="parallel", parallel=2)
+ # therefore running sphinx-build externally
+ indir = rootdir / "test-parallel"
+ run(["sphinx-build", "-b", "html", indir.as_posix(), tmp_path.as_posix(), "-j", "auto"], check=True)
+
+ # And posts are not collected by Ablog...
+ html = (tmp_path / "postlist.html").read_text(encoding="utf-8")
+ assert "post 1" not in html
+ assert "post 2" not in html
+ assert "post 3" not in html
+ assert "post 4" not in html
diff --git a/src/ablog/tests/test_postlist.py b/src/ablog/tests/test_postlist.py
new file mode 100644
index 0000000..b47d627
--- /dev/null
+++ b/src/ablog/tests/test_postlist.py
@@ -0,0 +1,38 @@
+import pytest
+
+
+def read_text(path):
+ """
+ Support function to give backward compatibility with older sphinx (v2).
+ """
+ if hasattr(path, "read_text"):
+ return path.read_text()
+ return path.text()
+
+
+@pytest.mark.sphinx("html", testroot="postlist") # using roots/test-postlist
+def test_postlist(app, status, warning):
+ app.build()
+
+ assert app.statuscode == 0
+ assert (app.outdir / "postlist.html").exists()
+
+ html = read_text(app.outdir / "postlist.html")
+ assert '<ul class="postlist-style-none postlist simple">' in html
+ assert (
+ '<li class="ablog-post"><p class="ablog-post-title">01 December - <a class="reference internal" href="post.html">post</a></p></li>'
+ in html
+ )
+
+
+@pytest.mark.sphinx("html", testroot="postlist", confoverrides={"post_date_format_short": "%Y-%m-%d"})
+def test_postlist_date_format_conf(app, status, warning):
+ app.build()
+
+ assert app.statuscode == 0
+
+ html = read_text(app.outdir / "postlist.html")
+ assert (
+ '<li class="ablog-post"><p class="ablog-post-title">2020-12-01 - <a class="reference internal" href="post.html">post</a></p></li>'
+ in html
+ )
diff --git a/src/ablog/tests/test_templates.py b/src/ablog/tests/test_templates.py
new file mode 100644
index 0000000..41f0e3b
--- /dev/null
+++ b/src/ablog/tests/test_templates.py
@@ -0,0 +1,82 @@
+from pathlib import Path
+import pytest
+from sphinx.application import Sphinx
+from sphinx.errors import ThemeError
+
+
+@pytest.mark.sphinx("html", testroot="templates", confoverrides={"skip_injecting_base_ablog_templates": True})
+def test_skip_ablog_templates_but_missing_templates(app: Sphinx):
+ """
+ Completely override the templates used by ablog, but not provide them.
+ """
+ with pytest.raises(
+ ThemeError,
+ match=r"An error happened in rendering the page blog/author\.\nReason: TemplateNotFound\(\"'ablog/catalog.html' not found in *.",
+ ):
+ app.build()
+
+
+@pytest.mark.sphinx(
+ "html",
+ testroot="templates",
+ confoverrides={
+ "templates_path": ["_templates"],
+ "skip_injecting_base_ablog_templates": False, # default value
+ "html_sidebars": {
+ "**": [
+ # overridden by user
+ "ablog/postcard.html",
+ # fallback to builtin
+ "ablog/authors.html",
+ ]
+ },
+ },
+)
+def test_override_template_but_fallback_missing(app: Sphinx, rootdir: Path):
+ """
+ Partially override the only some Ablog templates, but use the Ablog ones
+ for missing as fallback.
+ """
+ app.build()
+
+ # is the customized template it in the output?
+ customized = (rootdir / "test-templates" / "_templates" / "ablog" / "postcard.html").read_text()
+ source = (app.outdir / "post.html").read_text()
+ assert customized in source
+
+ # is builtin template in the output?
+ builtin = '<div class="ablog-sidebar-item ablog__authors">'
+ assert builtin in source
+
+
+@pytest.mark.sphinx(
+ "html",
+ testroot="templates",
+ confoverrides={
+ "html_sidebars": {
+ "**": [
+ # overridden by theme
+ "ablog/postcard.html",
+ # fallback to builtin
+ "ablog/authors.html",
+ ]
+ },
+ "html_theme_path": "_themes",
+ "html_theme": "test_theme",
+ },
+)
+def test_themes_templates_come_first(app: Sphinx, rootdir: Path):
+ """
+ Ensures that if theme supplies own Ablog template, it is used over the
+ builtin one, but fallback to builtin for missing ones.
+ """
+ app.build()
+
+ # is the customized template it in the output?
+ customized = (rootdir / "test-templates" / "_themes" / "test_theme" / "ablog" / "postcard.html").read_text()
+ source = (app.outdir / "post.html").read_text()
+ assert customized in source
+
+ # is builtin template in the output?
+ builtin = '<div class="ablog-sidebar-item ablog__authors">'
+ assert builtin in source
diff --git a/src/ablog/version.py b/src/ablog/version.py
new file mode 100644
index 0000000..e86557f
--- /dev/null
+++ b/src/ablog/version.py
@@ -0,0 +1,17 @@
+try:
+ from ._version import version
+except Exception:
+ import warnings
+
+ warnings.warn(f'could not determine {__name__.split(".")[0]} package version; this indicates a broken installation')
+ del warnings
+
+ version = "0.0.0"
+
+from packaging.version import parse as _parse
+
+_version = _parse(version)
+major, minor, bugfix = [*_version.release, 0][:3]
+release = not _version.is_devrelease
+
+__all__ = ["version", "major", "minor", "bugfix", "release"]
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..9992337
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,42 @@
+[tox]
+envlist =
+ py{39,310,311,312}{-sphinx6,-sphinx7,-sphinx8,-sphinx9,-devdeps,-docs,-linkcheck,-pydata-sphinx-theme}
+
+[testenv]
+allowlist_externals =
+ make
+ git
+extras =
+ dev
+commands =
+ # Have to do this here as myst-parser in the install step forces the version.
+ sphinx6: pip install -U "sphinx>=6.0,<7.0"
+ sphinx7: pip install -U "sphinx>=7.0,<8.0"
+ sphinx8: pip install -U "sphinx>=8.0,<9.0"
+ sphinx9: pip install -U "sphinx>=9.0,<10.0"
+ devdeps: pip install -U "docutils @ git+https://github.com/docutils/docutils.git\#\&subdirectory=docutils"
+ devdeps: pip install -U "git+https://github.com/sphinx-doc/sphinx"
+ pip freeze --all --no-input
+ pytest -vvv -r a --pyargs ablog
+ make tests
+
+[testenv:docs]
+changedir = docs
+description = Invoke sphinx-build to build the HTML docs
+commands =
+ sphinx-build -j auto --color -W --keep-going -b html -d _build/.doctrees . _build/html {posargs}
+ python -c 'import pathlib; print("Documentation available under file://\{0\}".format(pathlib.Path(r"{toxinidir}") / "docs" / "_build" / "index.html"))'
+
+[testenv:pydata-sphinx-theme]
+commands =
+ rm -rf pydata-sphinx-theme || true
+ git clone git@github.com:pydata/pydata-sphinx-theme.git --depth 1 pydata-sphinx-theme
+ pip install -e "pydata-sphinx-theme/.[dev]"
+ pip freeze --all --no-input
+ sphinx-build --color -W --keep-going -b html -d _build/.doctrees pydata-sphinx-theme/docs pydata-sphinx-theme/docs/_build/html {posargs}
+
+[testenv:linkcheck]
+changedir = docs
+description = Invoke sphinx-build to check linkcheck works
+commands =
+ sphinx-build --color -W --keep-going -b linkcheck . _build/html {posargs}