├── .github ├── FUNDING.yml └── workflows │ ├── contributing.yml │ ├── latest-changes.yml │ └── test.yml ├── .gitignore ├── .python-version ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── changelog.md ├── pyproject.toml ├── src └── fastapi_blog │ ├── __init__.py │ ├── helpers.py │ ├── main.py │ ├── router.py │ └── templates │ ├── 404.html │ ├── index.html │ ├── layout │ └── base.html │ ├── page.html │ ├── partials │ └── _post_short.html │ ├── post.html │ ├── posts.html │ ├── tag.html │ └── tags.html └── tests ├── __init__.py ├── examples ├── defaults.py ├── favorite_post_ids.py ├── modified_all.py ├── pages │ └── about.md ├── posts │ ├── 2023-01-configuring-sphinx-auto-doc-with-django.md │ ├── 2023-01-converting-markdown-headers-to-checklist.md │ ├── 2023-01-practice-python-web-projects.md │ ├── 2023-01-resolutions.md │ ├── 2023-02-programming-languages-ive-learned.md │ ├── 2023-04-aws-requests-auth.md │ ├── 2023-04-cookiecutter-options-pattern.md │ ├── 2023-04-my-fitness-journey.md │ ├── 2023-06-bjj-training-tips.md │ ├── 2023-06-converting-from-bleach-to-nh3.md │ ├── 2023-07-visit-to-paultons-park.md │ ├── 2023-08-pypi-project-urls-cheatsheet.md │ ├── 2023-10-we-moved-to-london.md │ ├── 2023-11-minimal-css-libraries.md │ ├── 2023-11-splitting-git-commit.md │ ├── 2023-11-three-years-at-kraken-tech.md │ ├── code-code-code.md │ ├── no-tags.md │ └── thirty-minute-rule.md ├── prefix_change.py ├── prefix_none.py ├── static │ ├── custom.css │ ├── normalize.css │ ├── pygments.css │ └── sakura.css └── templates │ └── layout │ └── base.html ├── test_helpers.py └── test_router.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: pydanny 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | polar: # Replace with a single Polar username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/workflows/contributing.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Contributing 3 | on: 4 | push: 5 | branches: [main] 6 | jobs: 7 | contrib-readme-job: 8 | runs-on: ubuntu-latest 9 | name: A job to automate contrib in readme 10 | steps: 11 | - name: Contribute List 12 | uses: akhilmhdh/contributors-readme-action@v2.3.6 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.REPO_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/latest-changes.yml: -------------------------------------------------------------------------------- 1 | name: Latest Changes 2 | 3 | on: 4 | pull_request_target: 5 | branches: 6 | - main 7 | # Or use the branch "master" if that's your main branch: 8 | # - master 9 | types: 10 | - closed 11 | # For manually triggering it 12 | workflow_dispatch: 13 | inputs: 14 | number: 15 | description: PR number 16 | required: true 17 | 18 | jobs: 19 | latest-changes: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | token: ${{ secrets.REPO_TOKEN }} 25 | - uses: tiangolo/latest-changes@0.2.1 26 | with: 27 | token: ${{ secrets.REPO_TOKEN }} 28 | latest_changes_file: changelog.md 29 | latest_changes_header: '# Changelog' -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | on: [push] 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | python-version: ['3.10', '3.11', '3.12'] 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Python ${{ matrix.python-version }} 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: ${{ matrix.python-version }} 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install '.[dev]' 20 | - name: Lint with ruff 21 | run: | 22 | # stop the build if there are linting errors 23 | make lint 24 | - name: Test with pytest 25 | run: |- 26 | pytest . 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | .hypothesis/ 163 | .vscode/ 164 | .ruff_cache/ 165 | changelog.json 166 | .idea 167 | .DS_Store 168 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for building a FastAPI Blog image 2 | # This is a multi-stage build 3 | # The first stage is to build the application 4 | # The second stage is to run the application 5 | 6 | # Use the official image as a parent image 7 | FROM python:3.12-alpine AS builder 8 | 9 | # Set the working directory in the container 10 | WORKDIR /app 11 | 12 | # Copy the current directory contents into the container at /app 13 | COPY . /app 14 | 15 | # Install make 16 | RUN apk add --no-cache make 17 | 18 | # Install fastapi-blog for local development 19 | # This installs any needed packages specified in pyproject.toml 20 | # as local development dependencies 21 | RUN make install 22 | 23 | # Run tests 24 | RUN make test 25 | 26 | # Configure the container to run the application 27 | ENV PORT=8000 28 | 29 | # Expose the port the app runs on 30 | EXPOSE 8000 31 | 32 | # Run the application 33 | # RUN make run 34 | # CMD uvicorn app.main:app --host 0.0.0.0 --port ${PORT} 35 | CMD uvicorn tests.example.main:app --host 0.0.0.0 --port ${PORT} 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Functional Source License, Version 1.1, Apache 2.0 Future License 2 | 3 | ## Abbreviation 4 | 5 | FSL-1.1-Apache-2.0 6 | 7 | ## Notice 8 | 9 | Copyright 2008-2024 Daniel Roy Greenfeld 10 | 11 | ## Terms and Conditions 12 | 13 | ### Licensor ("We") 14 | 15 | The party offering the Software under these Terms and Conditions. 16 | 17 | ### The Software 18 | 19 | The "Software" is each version of the software that we make available under 20 | these Terms and Conditions, as indicated by our inclusion of these Terms and 21 | Conditions with the Software. 22 | 23 | ### License Grant 24 | 25 | Subject to your compliance with this License Grant and the Patents, 26 | Redistribution and Trademark clauses below, we hereby grant you the right to 27 | use, copy, modify, create derivative works, publicly perform, publicly display 28 | and redistribute the Software for any Permitted Purpose identified below. 29 | 30 | ### Permitted Purpose 31 | 32 | A Permitted Purpose is any purpose other than a Competing Use. A Competing Use 33 | means making the Software available to others in a commercial product or 34 | service that: 35 | 36 | 1. substitutes for the Software; 37 | 38 | 2. substitutes for any other product or service we offer using the Software 39 | that exists as of the date we make the Software available; or 40 | 41 | 3. offers the same or substantially similar functionality as the Software. 42 | 43 | Permitted Purposes specifically include using the Software: 44 | 45 | 1. for your internal use and access; 46 | 47 | 2. for non-commercial education; 48 | 49 | 3. for non-commercial research; and 50 | 51 | 4. in connection with professional services that you provide to a licensee 52 | using the Software in accordance with these Terms and Conditions. 53 | 54 | ### Patents 55 | 56 | To the extent your use for a Permitted Purpose would necessarily infringe our 57 | patents, the license grant above includes a license under our patents. If you 58 | make a claim against any party that the Software infringes or contributes to 59 | the infringement of any patent, then your patent license to the Software ends 60 | immediately. 61 | 62 | ### Redistribution 63 | 64 | The Terms and Conditions apply to all copies, modifications and derivatives of 65 | the Software. 66 | 67 | If you redistribute any copies, modifications or derivatives of the Software, 68 | you must include a copy of or a link to these Terms and Conditions and not 69 | remove any copyright notices provided in or with the Software. 70 | 71 | ### Disclaimer 72 | 73 | THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR 74 | IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR 75 | PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT. 76 | 77 | IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE 78 | SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, 79 | EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE. 80 | 81 | ### Trademarks 82 | 83 | Except for displaying the License Details and identifying us as the origin of 84 | the Software, you have no right under these Terms and Conditions to use our 85 | trademarks, trade names, service marks or product names. 86 | 87 | ## Grant of Future License 88 | 89 | We hereby irrevocably grant you an additional license to use the Software under 90 | the Apache License, Version 2.0 that is effective on the second anniversary of 91 | the date we make the Software available. On or after that date, you may use the 92 | Software under the Apache License, Version 2.0, in which case the following 93 | will apply: 94 | 95 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 96 | this file except in compliance with the License. 97 | 98 | You may obtain a copy of the License at 99 | 100 | http://www.apache.org/licenses/LICENSE-2.0 101 | 102 | Unless required by applicable law or agreed to in writing, software distributed 103 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 104 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 105 | specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | VERSION=v$(shell grep -m 1 version pyproject.toml | tr -s ' ' | tr -d '"' | tr -d "'" | cut -d' ' -f3) 4 | 5 | tag: 6 | echo "Tagging version $(VERSION)" 7 | git tag -a $(VERSION) -m "Creating version $(VERSION)" 8 | git push origin $(VERSION) 9 | 10 | install: 11 | pip install uv 12 | uv pip install -e '.[dev]' 13 | 14 | all: 15 | make lint 16 | make mypy 17 | make test 18 | 19 | lint: 20 | ruff check . 21 | ruff format . --check 22 | 23 | format: 24 | ruff check . --fix 25 | ruff format . 26 | 27 | mypy: 28 | mypy . 29 | 30 | test: 31 | coverage run -m pytest . 32 | coverage report -m 33 | coverage html 34 | 35 | test-pdb: 36 | pytest --pdb . 37 | 38 | run: 39 | make run_defaults 40 | 41 | run_defaults: 42 | cd tests/examples && uvicorn defaults:app --reload 43 | 44 | run_modified_all: 45 | cd tests/examples && uvicorn modified_all:app --reload 46 | 47 | run_prefix_change: 48 | cd tests/examples && uvicorn prefix_change:app --reload 49 | 50 | run_prefix_none: 51 | cd tests/examples && uvicorn prefix_none:app --reload 52 | 53 | run_favorite_post_ids: 54 | cd tests/examples && uvicorn favorite_post_ids:app --reload -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastAPI Blog 2 | 3 | A simple, easy-to-use blog application built with FastAPI. 4 | 5 | ## Features 6 | 7 | - Write blog posts in Markdown 8 | - Syntax highlighting for code blocks 9 | - Responsive design 10 | - Dark mode 11 | - Overloadable templates 12 | - [Live, working configuration examples](https://github.com/pydanny/fastapi-blog/tree/main/tests/examples) 13 | - SEO-friendly 14 | - Sitemap 15 | - Docker support 16 | 17 | ## Basic Usage 18 | 19 | 1. Import the `add_blog_to_fastapi` function 20 | 2. Run the instantiated FastAPI app through the `add_blog_to_fastapi` function 21 | 22 | This all you need to do: 23 | 24 | ```python 25 | from fastapi_blog import add_blog_to_fastapi 26 | from fastapi import FastAPI 27 | 28 | 29 | app = FastAPI() 30 | app = add_blog_to_fastapi(app) 31 | 32 | 33 | @app.get("/") 34 | async def index() -> dict: 35 | return { 36 | "message": "Check out the blog at the URL", 37 | "url": "http://localhost:8000/blog", 38 | } 39 | ``` 40 | 41 | 3. Add the first blog entry 42 | 43 | Assuming your FastAPI app is defined in a `main.py` module at the root of your project, create a file at `posts/first-blog-post.md`: 44 | 45 | ```markdown 46 | --- 47 | date: "2024-03-21T22:20:50.52Z" 48 | published: true 49 | tags: 50 | - fastapi 51 | - fastapi-blog 52 | title: First blog post 53 | description: This is the first blog post entry. 54 | --- 55 | 56 | Exciting times in the world of fastapi-blog are ahead! 57 | 58 | ## This is a markdown header 59 | 60 | And this is a markdown paragraph with a [link](https://github.com/pydanny/fastapi-blog). 61 | ``` 62 | 63 | 4. Add the first page 64 | 65 | Assuming your FastAPI app is defined in a `main.py` module at the root of your project, create a file at `pages/about.md`: 66 | 67 | ```markdown 68 | --- 69 | title: "About Me" 70 | description: "A little bit of background about me" 71 | author: "Daniel Roy Greenfeld" 72 | --- 73 | 74 | ## Intro about me 75 | 76 | I'm probably best known as "[pydanny](https://www.google.com/search?q=pydanny)", one of the authors of Two Scoops of Django. 77 | 78 | I love to hang out with my [wife](https://audrey.feldroy.com/), play with my [daughter](/tags/uma), do [Brazilian Jiu-Jitsu](https://academyofbrazilianjiujitsu.com/), write [books](/books), and read books. 79 | 80 | - [Mastodon](https://fosstodon.org/@danielfeldroy) 81 | - [LinkedIn](https://www.linkedin.com/in/danielfeldroy/) 82 | - [Twitter](https://twitter.com/pydanny) 83 | 84 | ## About this site 85 | 86 | This site is written in: 87 | 88 | - Python 89 | - FastAPI 90 | - fastapi-blog 91 | - Sakura minimal CSS framework 92 | - Markdown 93 | - Vanilla HTML 94 | ``` 95 | 96 | 97 | ## Advanced Usage 98 | 99 | fastapi_blog is configurable through the `add_blog_to_fastapi` function. 100 | 101 | ### Adding app-controlled static media 102 | 103 | Change the main app to mount StaticFiles: 104 | 105 | ```python 106 | from fastapi_blog import add_blog_to_fastapi 107 | from fastapi import FastAPI 108 | from fastapi.staticfiles import StaticFiles 109 | 110 | 111 | app = FastAPI() 112 | app = add_blog_to_fastapi(app) 113 | app.mount("/static", StaticFiles(directory="static"), name="static") 114 | 115 | @app.get("/") 116 | async def index() -> dict: 117 | return { 118 | "message": "Check out the blog at the URL", 119 | "url": "http://localhost:8000/blog", 120 | } 121 | ``` 122 | 123 | ### Replacing the default templates 124 | 125 | This example is Django-like in that your local templates will overload the default ones. 126 | 127 | ```python 128 | import fastapi_blog 129 | import jinja2 130 | from fastapi import FastAPI 131 | from fastapi.staticfiles import StaticFiles 132 | 133 | 134 | django_style_jinja2_loader = jinja2.ChoiceLoader( 135 | [ 136 | jinja2.FileSystemLoader("templates"), 137 | jinja2.PackageLoader("fastapi_blog", "templates"), 138 | ] 139 | ) 140 | 141 | app = FastAPI() 142 | app = fastapi_blog.add_blog_to_fastapi( 143 | app, prefix=prefix, jinja2_loader=django_style_jinja2_loader 144 | ) 145 | app.mount("/static", StaticFiles(directory="static"), name="static") 146 | 147 | 148 | @app.get("/") 149 | async def index() -> dict: 150 | return { 151 | "message": "Check out the blog at the URL", 152 | "url": f"http://localhost:8000/blog", 153 | } 154 | ``` 155 | 156 | 157 | ### Changing the location of the blog url 158 | 159 | Perhaps you want to have the blog at the root? 160 | 161 | ```python 162 | import fastapi_blog 163 | from fastapi import FastAPI 164 | 165 | 166 | app = FastAPI() 167 | app = fastapi_blog.add_blog_to_fastapi( 168 | app, prefix="change" 169 | ) 170 | 171 | 172 | @app.get("/api") 173 | async def index() -> dict: 174 | return { 175 | "message": "Check out the blog at the URL", 176 | "url": "http://localhost:8000/change", 177 | } 178 | ``` 179 | 180 | ## Blog at root URL 181 | 182 | This is for when your blog/CMS needs to be at the root of the project 183 | 184 | ```python 185 | import fastapi_blog 186 | from fastapi import FastAPI 187 | 188 | 189 | app = FastAPI() 190 | 191 | 192 | @app.get("/api") 193 | async def index() -> dict: 194 | return { 195 | "message": "Check out the blog at the URL", 196 | "url": "http://localhost:8000", 197 | } 198 | 199 | # Because the prefix is None, the call to add_blog_to_fastapi 200 | # needs to happen after the other view functions are defined. 201 | app = fastapi_blog.add_blog_to_fastapi(app, prefix=None) 202 | ``` 203 | 204 | 205 | ## Add favorite articles to the homepage 206 | 207 | ```python 208 | import fastapi_blog 209 | from fastapi import FastAPI 210 | 211 | 212 | favorite_post_ids = { 213 | "code-code-code", 214 | "thirty-minute-rule", 215 | "2023-11-three-years-at-kraken-tech", 216 | } 217 | 218 | app = FastAPI() 219 | app = fastapi_blog.add_blog_to_fastapi(app, favorite_post_ids=favorite_post_ids) 220 | 221 | 222 | @app.get("/") 223 | async def index() -> dict: 224 | return { 225 | "message": "Check out the blog at the URL", 226 | "url": "http://localhost:8000/blog", 227 | } 228 | ``` 229 | 230 | ### Add page not in the blog list of posts 231 | 232 | In the `pages` directory of your blog, add markdown files with frontmatter. You can then find it by going to the URL with that name. For example, adding this `pages/about.md` to the default config would make this appear at http://localhost:8000/blog/about. 233 | 234 | ```markdown 235 | --- 236 | title: "About Daniel Roy Greenfeld" 237 | description: "A little bit of background about Daniel Roy Greenfeld" 238 | author: "Daniel Roy Greenfeld" 239 | --- 240 | 241 | I'm probably best known as "[pydanny](https://www.google.com/search?q=pydanny)", one of the authors of [Two Scoops of Django](/books/tech). 242 | ``` 243 | 244 | 245 | ## Installation and Running Example Sites 246 | 247 | ### Option 1: Local Virtualenv 248 | 249 | You can install this into a virtualenv using the pyproject.toml file: 250 | 251 | ```bash 252 | pip install fastapi-blog 253 | make run 254 | ``` 255 | 256 | ### Option 2: Docker (Local Dockerfile) 257 | 258 | Or into a Docker container using the local Dockerfile: 259 | 260 | ```bash 261 | docker build -t fastapi-blog . 262 | docker run -d -p 8000:8000 fastapi-blog 263 | ``` 264 | 265 | ### Option 3: Docker (Prebuilt) 266 | 267 | Or using a prebuilt Docker image from GitHub Container Registry: 268 | 269 | ```bash 270 | docker run -d -p 8000:8000 ghcr.io/aroygreenfeld/fastapi-blog:latest 271 | ``` 272 | 273 | This is if you just want to run the application without building it yourself. 274 | 275 | ## Releasing a new version 276 | 277 | 1. Update the version in `pyproject.toml` and `fastapi_blog/__init__.py` 278 | 279 | 2. Update changelog.md 280 | 281 | 3. Build the distribution locally: 282 | 283 | ```bash 284 | rm -rf dist 285 | pip install -U build 286 | python -m build 287 | ``` 288 | 289 | 4. Upload the distribution to PyPI: 290 | 291 | ```bash 292 | pip install -U twine 293 | python -m twine upload dist/* 294 | ``` 295 | 296 | 5. Create a new release on GitHub and tag the release: 297 | 298 | ```bash 299 | git commit -am "Release for vXYZ" 300 | make tag 301 | ``` 302 | 303 | ## Contributors 304 | 305 | 306 |
309 |
310 | 312 | Daniel Roy Greenfeld 313 | 314 | |
315 |
316 |
317 | 319 | Audrey Roy Greenfeld 320 | 321 | |
70 | 71 | About 72 | 73 | | 74 | 75 | Articles 76 | 77 | | 78 | 79 | Tags 80 | 81 |
82 |{{ page.metadata.description }}
7 | {% endif %} 8 | 9 | {% if page.metadata.image %} 10 |
4 | {% if post.description %}
5 | {{ post.description }}
6 | {% endif %}
7 |
8 | {% if post.tags %}
9 | Tags: {% for tag in post.tags %}
10 | {{ tag }}
11 | {% endfor %}
12 | {% endif %}
13 |
Tags: 10 | 11 | 12 | {% for tag in post.tags %} 13 | {{ tag }} 14 | {% endfor %} 15 |
16 | {% endif %} 17 | 18 | {% endblock content %} -------------------------------------------------------------------------------- /src/fastapi_blog/templates/posts.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | {% block content %} 3 |` tags. 18 | 19 | The result is closer to the HTML specification and hence easier to read. Definately fits in with the concepts of [HTML First](https://html-first.com/). 20 | 21 | One feature I can't wait to implement is a dark mode controlled by media query (or a toggle). Before it meant tracking down all the CSS components, now it's just pointing at different Sakura themes. 22 | 23 | As for how it looks, check out the before and after images below: 24 | 25 | **Old 100% handcrafted CSS** 26 | 27 | I was never happy with the spacing between elements, and struggled with all the CSS bits to make it work. 28 | 29 |  30 | 31 | **Minimal CSS library + a few custom classes** 32 | 33 | In the new version the default spacing is really nice, and my only challenge was getting my avatar image to come down `2.5 rem`. 34 | 35 |  36 | 37 | ## Resources: 38 | 39 | - [Sakura](https://github.com/oxalorg/sakura) - CSS framework now helping to style this site 40 | - [Drop-in Minimal CSS comparison framework site](https://dohliam.github.io/dropin-minimal-css/?sakura) 41 | 42 | -------------------------------------------------------------------------------- /tests/examples/posts/2023-11-splitting-git-commit.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: "2023-11-13T14:00:00.00Z" 3 | published: true 4 | slug: 2023-11-splitting-git-commit 5 | tags: 6 | - git 7 | - howto 8 | time_to_read: 5 9 | title: Splitting Git Commits 10 | description: How to split a git commit into multiple commits. 11 | type: post 12 | --- 13 | 14 | Something I periodically do is split up a commit into multiple commits. I am documenting it here where I can find it fast. 15 | 16 | I didn't come up with this technique, sources can be found [here](https://stackoverflow.com/questions/6217156/break-a-previous-commit-into-multiple-commits), [here](https://www.internalpointers.com/post/split-commit-into-smaller-ones-git), and [here](https://dev.to/timmouskhelichvili/how-to-split-a-git-commit-into-multiple-ones-3g6f). All credit goes to those sources. 17 | 18 | ## Break up most recent commit 19 | 20 | To reset the most recent commit: 21 | 22 | ```sh 23 | git reset HEAD~ 24 | ``` 25 | 26 | This reverts the commit. Commit individual or groups of files in multiple commits. 27 | 28 | 29 | ## What if your commit isn't the most recent one? 30 | 31 | Use an interactive rebase to commit things further back. Follow these steps: 32 | 33 | 1. Find the commit hash with either `git log` or `git reflog` 34 | 35 | 2. Start a rebase with the commit hash replacing `HASH` below 36 | 37 | ```sh 38 | git rebase -i HASH 39 | ``` 40 | 41 | 3. In the rebase edit screen that comes up, find the line with the commit you want to split. Replace `pick` with `edit` 42 | 43 | 4. Save and close the rebase edit screen 44 | 45 | 5. Reset to the previous commit with `git rebase HEAD~` 46 | 47 | 6. Create new commits using the files and writing the messages that go along with them 48 | 49 | ```sh 50 | git add file/to/be/committed.xyz 51 | git commit -m "Enhance the algorithm to support Mars rovers" 52 | ``` 53 | 54 | 7. Finish the rebase: 55 | 56 | ```sh 57 | git rebase --continue 58 | ``` -------------------------------------------------------------------------------- /tests/examples/posts/2023-11-three-years-at-kraken-tech.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: "2023-11-23T22:20:50.52Z" 3 | published: true 4 | tags: 5 | - octopus 6 | - kraken 7 | - climate-change 8 | - python 9 | - django 10 | time_to_read: 5 11 | title: Three Years at Kraken Tech 12 | description: A summary of the past year as I finish my third year working for Kraken Tech, an Octopus Energy Group subsidiary. 13 | image: /logos/kraken-logo-dark.png 14 | twitter_image: /logos/kraken-logo-dark.png 15 | og_url: https://daniel.feldroy.com/posts/2023-11-three-years-at-octopus 16 | --- 17 | 18 | A summary of the past year as I finish my third year working for [Kraken Tech](https://kraken.tech/), an [Octopus Energy Group](https://octopusenergy.group/) subsidiary. 19 | 20 | _Note: As I write this I'm recovering from a sprained shoulder I received in a bicycle accident that makes it hard to type. I'm mostly using voice-to-text, so the text might not be in my normal "writing voice"._ 21 | 22 | # I changed roles 23 | 24 | I transitioned from leading the tech team for Kraken Tech USA to being an individual contributor for the global team. I do this periodically, cycling between writing code to leading teams, and back again. 25 | 26 | It is never easy to transition between roles but this time was particularly hard. The US Kraken Tech team is a group of talented, diverse, and passionate people. I'm so honored I had the chance to work with them. I'm constantly amazed by what they have accomplished and what they continue to do. 27 | 28 | In my new role I'm on the "Developer Foundations" team, exploring ways to improve the coding experience at the company. I've enjoyed writing code full-time again, there's a clarity of purpose that's been fun to embrace. Mostly I'm coding in Python with a bit of Django, with short detours into HCL, Rust, and Golang. 29 | 30 | One more thing: For the moment I still have the old management-sounding title, but expect to get something more accurate in the next few months. 31 | 32 | # I blogged more 33 | 34 | As I'm coding full-time again, in 2023 I wrote more than double the number of posts I wrote in 2022. 35 | 36 | # I worked in the London, UK Office 37 | 38 | Audrey and Uma joined me on a move to London, which I documented [here](https://daniel.feldroy.com/posts/2023-10-we-moved-to-london). 39 | 40 | It has been a delight being able to work in-person with my peers, or at least close in timezone. I'm usually in the office about three out of every five days. I would make it five-out-of-five but I do better with deep thinking outside normal office commotion. 41 | 42 | # I attended large in-person tech events 43 | 44 | After a break of four years I managed to attend the following in chronological order, with transit mode specified: 45 | 46 | - Django London (walk or train) 47 | - PyCon Italy (airplane) 48 | - PyCon UK (train) 49 | - Django Paris (train) 50 | 51 | I very much enjoyed meeting old friends and making new ones. Plus, exploring the cities of London, Cardiff, and Paris were adventures of their own. 52 | 53 | # Helped scale up Kraken Tech to address climate change 54 | 55 | As an individual concerned about global climate change I do my part: 56 | 57 | - I compost about 80% of my family's food waste 58 | - Do our best to recyle all the family's cardboard and paper products 59 | - We live in a tiny flat with an electric stove 60 | - Am experimenting with using a portable electric heater instead of the methane gas boiler 61 | - Have reduced unnecessary air travel as much as possible (which is hard - I love airplanes and airports) 62 | 63 | That's great, but I have managed to scale that up tens of thousands of times over by working for Kraken Tech. 64 | 65 | As one employee of nine hundred and thirty seven (937) at Kraken Tech, I take payment from a collective of companies whose products are all designed with the purpose of saving the planet. Kraken Tech in particular is responsible for about 55 million people getting onto renewable energy or using non-renewable resources more efficiently. 66 | 67 | Let's math that out, using conservative numbers: 68 | 69 | ``` 70 | 58,697.97 = 55,000,000 / 937 71 | ``` 72 | 73 | **Summary: Each employee of Kraken Tech is responsible for 58,698 people contributing significantly less to global climate change.** 74 | 75 | So if you are in tech in any capacity (coding, management, etc), consider using your skills to help us in our mission of making a healthier planet. 76 | 77 | # Come and help me save the planet 78 | 79 | I'll be upfront that if you want to make the biggest salary possible in tech, travel a lot, and fly in first place seats, go seek employment with a [Big Tech Company](https://en.wikipedia.org/wiki/Big_Tech) or a fintech. Personally, I would rather work toward making the planet better for our descendants. That to me is much more important than helping a billionaire score another billion. 80 | 81 | I invite anyone who is reading this to join me on my mission of saving the planet. Our careers page is [here](https://octopus.energy/careers). 82 | 83 | _Please understand that Kraken Tech only hires in countries in which we have a legal entity. So if a role isn't listed for the country in which you can legally work, we probably can't hire you._ 84 | 85 | If for whatever reason you can't get a job working with me, [apply to work at somewhere else into decarbonization](https://climatebase.org/). Let me know where you go and I'll celebrate your joining our shared mission. 86 | 87 | [](https://kraken.tech) -------------------------------------------------------------------------------- /tests/examples/posts/code-code-code.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: "2016-05-28" 3 | published: true 4 | slug: code-code-code 5 | tags: 6 | - django 7 | - python 8 | - foxpro 9 | time_to_read: 3 10 | title: Code, Code, Code 11 | image: /images/code-code-code.png 12 | twitter_image: /images/code-code-code.png 13 | description: I'm often asked by new programmers how they can forge a path into using their skills professionally. Or how they can get better at writing software. In this article I share the secret master-level method to improvement. 14 | --- 15 | 16 | # How to Improve Your Coding Skills 17 | 18 | This was my path. It may not be your path. This path also isn't in any 19 | particular order, all of them apply from the moment you start on the 20 | path. 21 | 22 | 1. I coded. A lot. From silly little scripts to automating tasks to 23 | attempting full-blown projects. At work or for fun. I failed a lot, 24 | but learned along the way. 25 | 2. I didn't jump from language to language. Instead I stayed in a few 26 | places for years and focused my learning on those tools. My 19+ year 27 | career can be summed up as FoxPro then Java then Python. In the 28 | middle of things I picked up JavaScript. Sure, I've dallied with a 29 | few things (Lisp, Haskell, Lua, Perl, ColdFusion, Go), but by 30 | staying focused on a small set of tools I'm better than mediocre. 31 | 3. I coded lots. Yes, this is a repeat of #1. 32 | 4. Once I got the basics of a language, I looked up best practices for 33 | each of them. Then I religiously adhered to them, even becoming 34 | dogmatic about it. In general this means my code is more easily 35 | read. More easily debugged. And most importantly, more easily 36 | shared. 37 | 5. Did I mention that I coded a lot? You can never get good at anything 38 | unless you practice. Another repeat of #1. 39 | 6. I got over my fear/pride of asking questions. Well, mostly, I still 40 | am afraid/prideful from time to time. Honestly, by asking questions 41 | you aren't showing what you don't know, you are showing you are 42 | willing to learn. Also, the simple act of figuring out how to ask a 43 | question can put you in the right mindset to determine the answer 44 | yourself. 45 | 7. As soon as I asked a question, whether or not I got an answer, I 46 | coded some more. Code, code, code! Yet another repeat of #1 47 | 8. Once I've gotten the hang of a language, I looked for cookbooks 48 | and/or pocket references on it. I prefer paper copies of tech books 49 | (everything else I read is electronic). The recipes in cookbooks 50 | become the foundation of my toolkit. The terse, easy-to-find 51 | reminders in the pocket reference mean less cognitive overload. 52 | 9. I took those recipes and references and coded with them. Again and 53 | again I coded. In work hours or play time. Practice makes perfect! 54 | Why do I keep repeating #1? 55 | 10. Over the years I've stayed with the easiest-to-learn stable 56 | IDEs/text editors. Yes, I know there are really powerful tools with 57 | arcane commands (Vim, EMACS, etc), but I don't want to have to stop 58 | what I'm doing to learn new tools. I want to code, not tinker with 59 | desktop tools or arcane text editors. 60 | 11. And again, reference back to #1, I use the text editor to write 61 | code. Code, code, code! Until my fingers and hands hurt, until I've 62 | had to learn how to deal with carpal tunnel syndrome. Code, code, 63 | code! It's like learning martial arts, guitar, or anything, 64 | repetition of simple actions provides the confidence for you to 65 | either combine those actions into something greater or learn 66 | something more complex. 67 | 68 | # What I Wish I Had Done 69 | 70 | - Studied computer science. If I could do it all over again, that 71 | would have been the focus of my academic studies. It wouldn't 72 | replace anything on my list, the path I've defined remains the 73 | same. Code, code, code! 74 | - It goes without saying I should have taken more tutorials. Nothing 75 | gives a kickstart like having an instructor, online or in-person, 76 | who guides you down the right path. Then you can code, code, code! 77 | 78 | Practice makes perfect, right? 79 | 80 | [](/code-code-code.html) 81 | -------------------------------------------------------------------------------- /tests/examples/posts/no-tags.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: "2024-01-20" 3 | published: true 4 | slug: no-tags 5 | tags: 6 | time_to_read: 3 7 | title: No Tags 8 | description: A post with no tags 9 | --- 10 | 11 | This post has no tags -------------------------------------------------------------------------------- /tests/examples/posts/thirty-minute-rule.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: "2021-08-18" 3 | description: What to do when you get stuck on a coding issue for more than 30 minutes. 4 | published: true 5 | slug: thirty-minute-rule 6 | tags: 7 | - rant 8 | - tools 9 | - advice 10 | time_to_read: 3 11 | title: The Thirty Minute Rule 12 | image: /images/6933443849_51316a7cb7.jpg 13 | twitter_image: /images/6933443849_51316a7cb7.jpg 14 | --- 15 | 16 | When it comes to coding (or anything related to it) there's a rule I believe in with all my heart. It's the **Thirty Minute Rule**. 17 | 18 | The rule is that if anyone gets stuck on something for more than 30 minutes, they should ask for help. By asking for help after 30 minutes it addresses the following things: 19 | 20 | 1. Often we are stuck because of something we don’t know, making it impossible to proceed regardless of skill or intelligence. 21 | 2. Makes it so we don’t get frustrated by being stuck on a problem for too long. 22 | 3. Sometimes simply formulating the question to ask allows us to answer the problem ourselves. 23 | 4. From a business standpoint, reduces costs because instead of hours or days or weeks being stuck, we move on after 30 minutes. 24 | 25 | ## FAQ 26 | 27 | ### Who do I ask for help? 28 | 29 | Whoever you can. Co-workers and collaborators are common. If they don't have the answer, online discussion forums, groups, or general social media are useful. This was the original use case for [Stack Overflow](https://stackoverflow.com/). 30 | 31 | ### What if no one can give me an answer? 32 | 33 | Then you've got a real challenge. Solving hard problems is part of our job. 34 | 35 | ### Shouldn't people figure stuff out on their own? 36 | 37 | Often, that's impossible. Many years ago I once spent several days trying to figure out how to package up something for deployment. When I finally got up the nerve to ask for help, I learned that critical details of our environment weren't in the group wiki, but handwritten in the system administrators' notebook. While that was an admittedly insane scenario, it demonstrates that until I asked, there was no way I was ever going to accomplish my assigned task. 38 | 39 | Had I asked for help in thirty minutes, I wouldn't have experienced so much frustration. I wouldn't have wasted several days of work. 40 | 41 | ### I had to figure stuff out on my own, shouldn't other people? 42 | 43 | I've heard this argument before and it disappoints me. 44 | 45 | We should show compassion for others. Why would we want other people to suffer the same sort of frustration we did? 46 | 47 | We shouldn't repeat the mistakes of the past. 48 | 49 | ### What if someone asks me a question and I don't know the answer 50 | 51 | Then politely admit you don't know the answer. Gently steer the person to others who can help them, or to online help methods. 52 | 53 | ### What if someone asks me questions and I'm too busy to answer them? 54 | 55 | Gently guide the person to others who can help them, or to online help methods. Or suggest a time when you will have time to answer their questions. 56 | 57 | ### Does it have to be thirty minutes? 58 | 59 | Adjust the duration to best suit your team. A common variation is to increase it to 60 minutes. 60 | 61 |  62 | 63 | _Sitting at my first computer._ 64 | 65 | --- 66 | 67 | **Edits** 68 | 69 | - Grammar correction thanks to Vadym Khodak 70 | - Mention that it's okay to change the duration of the rule 71 | -------------------------------------------------------------------------------- /tests/examples/prefix_change.py: -------------------------------------------------------------------------------- 1 | import fastapi_blog 2 | from fastapi import FastAPI 3 | 4 | 5 | app = FastAPI() 6 | app = fastapi_blog.add_blog_to_fastapi(app, prefix="change") 7 | 8 | 9 | @app.get("/") 10 | async def index() -> dict: 11 | return { 12 | "message": "Check out the blog at the URL", 13 | "url": "http://localhost:8000/change", 14 | } 15 | -------------------------------------------------------------------------------- /tests/examples/prefix_none.py: -------------------------------------------------------------------------------- 1 | import fastapi_blog 2 | from fastapi import FastAPI 3 | 4 | 5 | app = FastAPI() 6 | 7 | 8 | @app.get("/api") 9 | async def index() -> dict: 10 | return { 11 | "message": "Check out the blog at the URL", 12 | "url": "http://localhost:8000", 13 | } 14 | 15 | 16 | # Because the prefix is None, the call to add_blog_to_fastapi 17 | # needs to happen after the other view functions are defined. 18 | app = fastapi_blog.add_blog_to_fastapi(app, prefix=None) 19 | -------------------------------------------------------------------------------- /tests/examples/static/custom.css: -------------------------------------------------------------------------------- 1 | a { 2 | color: #0070f3 !important; 3 | } 4 | 5 | header { 6 | text-align: center; 7 | } 8 | 9 | h1 { 10 | font-size: 2.5rem; 11 | line-height: 1.2; 12 | font-weight: 800; 13 | letter-spacing: -0.05rem; 14 | margin: 1rem 0; 15 | } 16 | 17 | h2 { 18 | font-size: 2rem; 19 | line-height: 1.3; 20 | font-weight: 800; 21 | letter-spacing: -0.05rem; 22 | margin: 1rem 0; 23 | } 24 | 25 | h3 { 26 | font-size: 1.5rem; 27 | line-height: 1.4; 28 | margin: 1rem 0; 29 | } 30 | 31 | h4 { 32 | font-size: 1.2rem; 33 | line-height: 1.5; 34 | } 35 | 36 | .borderCircle { 37 | border-radius: 9999px; 38 | margin-bottom: 0rem; 39 | text-decoration: none; 40 | } 41 | -------------------------------------------------------------------------------- /tests/examples/static/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Change the default font family in all browsers (opinionated). 5 | * 2. Correct the line height in all browsers. 6 | * 3. Prevent adjustments of font size after orientation changes in 7 | * IE on Windows Phone and in iOS. 8 | */ 9 | 10 | /* Document 11 | ========================================================================== */ 12 | 13 | html { 14 | font-family: sans-serif; /* 1 */ 15 | line-height: 1.15; /* 2 */ 16 | -ms-text-size-adjust: 100%; /* 3 */ 17 | -webkit-text-size-adjust: 100%; /* 3 */ 18 | } 19 | 20 | /* Sections 21 | ========================================================================== */ 22 | 23 | /** 24 | * Remove the margin in all browsers (opinionated). 25 | */ 26 | 27 | body { 28 | margin: 0; 29 | } 30 | 31 | /** 32 | * Add the correct display in IE 9-. 33 | */ 34 | 35 | article, 36 | aside, 37 | footer, 38 | header, 39 | nav, 40 | section { 41 | display: block; 42 | } 43 | 44 | /** 45 | * Correct the font size and margin on `h1` elements within `section` and 46 | * `article` contexts in Chrome, Firefox, and Safari. 47 | */ 48 | 49 | h1 { 50 | font-size: 2em; 51 | margin: 0.67em 0; 52 | } 53 | 54 | /* Grouping content 55 | ========================================================================== */ 56 | 57 | /** 58 | * Add the correct display in IE 9-. 59 | * 1. Add the correct display in IE. 60 | */ 61 | 62 | figcaption, 63 | figure, 64 | main { /* 1 */ 65 | display: block; 66 | } 67 | 68 | /** 69 | * Add the correct margin in IE 8. 70 | */ 71 | 72 | figure { 73 | margin: 1em 40px; 74 | } 75 | 76 | /** 77 | * 1. Add the correct box sizing in Firefox. 78 | * 2. Show the overflow in Edge and IE. 79 | */ 80 | 81 | hr { 82 | box-sizing: content-box; /* 1 */ 83 | height: 0; /* 1 */ 84 | overflow: visible; /* 2 */ 85 | } 86 | 87 | /** 88 | * 1. Correct the inheritance and scaling of font size in all browsers. 89 | * 2. Correct the odd `em` font sizing in all browsers. 90 | */ 91 | 92 | pre { 93 | font-family: monospace, monospace; /* 1 */ 94 | font-size: 1em; /* 2 */ 95 | } 96 | 97 | /* Text-level semantics 98 | ========================================================================== */ 99 | 100 | /** 101 | * 1. Remove the gray background on active links in IE 10. 102 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 103 | */ 104 | 105 | a { 106 | background-color: transparent; /* 1 */ 107 | -webkit-text-decoration-skip: objects; /* 2 */ 108 | } 109 | 110 | /** 111 | * Remove the outline on focused links when they are also active or hovered 112 | * in all browsers (opinionated). 113 | */ 114 | 115 | a:active, 116 | a:hover { 117 | outline-width: 0; 118 | } 119 | 120 | /** 121 | * 1. Remove the bottom border in Firefox 39-. 122 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 123 | */ 124 | 125 | abbr[title] { 126 | border-bottom: none; /* 1 */ 127 | text-decoration: underline; /* 2 */ 128 | text-decoration: underline dotted; /* 2 */ 129 | } 130 | 131 | /** 132 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 133 | */ 134 | 135 | b, 136 | strong { 137 | font-weight: inherit; 138 | } 139 | 140 | /** 141 | * Add the correct font weight in Chrome, Edge, and Safari. 142 | */ 143 | 144 | b, 145 | strong { 146 | font-weight: bolder; 147 | } 148 | 149 | /** 150 | * 1. Correct the inheritance and scaling of font size in all browsers. 151 | * 2. Correct the odd `em` font sizing in all browsers. 152 | */ 153 | 154 | code, 155 | kbd, 156 | samp { 157 | font-family: monospace, monospace; /* 1 */ 158 | font-size: 1em; /* 2 */ 159 | } 160 | 161 | /** 162 | * Add the correct font style in Android 4.3-. 163 | */ 164 | 165 | dfn { 166 | font-style: italic; 167 | } 168 | 169 | /** 170 | * Add the correct background and color in IE 9-. 171 | */ 172 | 173 | mark { 174 | background-color: #ff0; 175 | color: #000; 176 | } 177 | 178 | /** 179 | * Add the correct font size in all browsers. 180 | */ 181 | 182 | small { 183 | font-size: 80%; 184 | } 185 | 186 | /** 187 | * Prevent `sub` and `sup` elements from affecting the line height in 188 | * all browsers. 189 | */ 190 | 191 | sub, 192 | sup { 193 | font-size: 75%; 194 | line-height: 0; 195 | position: relative; 196 | vertical-align: baseline; 197 | } 198 | 199 | sub { 200 | bottom: -0.25em; 201 | } 202 | 203 | sup { 204 | top: -0.5em; 205 | } 206 | 207 | /* Embedded content 208 | ========================================================================== */ 209 | 210 | /** 211 | * Add the correct display in IE 9-. 212 | */ 213 | 214 | audio, 215 | video { 216 | display: inline-block; 217 | } 218 | 219 | /** 220 | * Add the correct display in iOS 4-7. 221 | */ 222 | 223 | audio:not([controls]) { 224 | display: none; 225 | height: 0; 226 | } 227 | 228 | /** 229 | * Remove the border on images inside links in IE 10-. 230 | */ 231 | 232 | img { 233 | border-style: none; 234 | } 235 | 236 | /** 237 | * Hide the overflow in IE. 238 | */ 239 | 240 | svg:not(:root) { 241 | overflow: hidden; 242 | } 243 | 244 | /* Forms 245 | ========================================================================== */ 246 | 247 | /** 248 | * 1. Change the font styles in all browsers (opinionated). 249 | * 2. Remove the margin in Firefox and Safari. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | font-family: sans-serif; /* 1 */ 258 | font-size: 100%; /* 1 */ 259 | line-height: 1.15; /* 1 */ 260 | margin: 0; /* 2 */ 261 | } 262 | 263 | /** 264 | * Show the overflow in IE. 265 | * 1. Show the overflow in Edge. 266 | */ 267 | 268 | button, 269 | input { /* 1 */ 270 | overflow: visible; 271 | } 272 | 273 | /** 274 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 275 | * 1. Remove the inheritance of text transform in Firefox. 276 | */ 277 | 278 | button, 279 | select { /* 1 */ 280 | text-transform: none; 281 | } 282 | 283 | /** 284 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 285 | * controls in Android 4. 286 | * 2. Correct the inability to style clickable types in iOS and Safari. 287 | */ 288 | 289 | button, 290 | html [type="button"], /* 1 */ 291 | [type="reset"], 292 | [type="submit"] { 293 | -webkit-appearance: button; /* 2 */ 294 | } 295 | 296 | /** 297 | * Remove the inner border and padding in Firefox. 298 | */ 299 | 300 | button::-moz-focus-inner, 301 | [type="button"]::-moz-focus-inner, 302 | [type="reset"]::-moz-focus-inner, 303 | [type="submit"]::-moz-focus-inner { 304 | border-style: none; 305 | padding: 0; 306 | } 307 | 308 | /** 309 | * Restore the focus styles unset by the previous rule. 310 | */ 311 | 312 | button:-moz-focusring, 313 | [type="button"]:-moz-focusring, 314 | [type="reset"]:-moz-focusring, 315 | [type="submit"]:-moz-focusring { 316 | outline: 1px dotted ButtonText; 317 | } 318 | 319 | /** 320 | * Change the border, margin, and padding in all browsers (opinionated). 321 | */ 322 | 323 | fieldset { 324 | border: 1px solid #c0c0c0; 325 | margin: 0 2px; 326 | padding: 0.35em 0.625em 0.75em; 327 | } 328 | 329 | /** 330 | * 1. Correct the text wrapping in Edge and IE. 331 | * 2. Correct the color inheritance from `fieldset` elements in IE. 332 | * 3. Remove the padding so developers are not caught out when they zero out 333 | * `fieldset` elements in all browsers. 334 | */ 335 | 336 | legend { 337 | box-sizing: border-box; /* 1 */ 338 | color: inherit; /* 2 */ 339 | display: table; /* 1 */ 340 | max-width: 100%; /* 1 */ 341 | padding: 0; /* 3 */ 342 | white-space: normal; /* 1 */ 343 | } 344 | 345 | /** 346 | * 1. Add the correct display in IE 9-. 347 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 348 | */ 349 | 350 | progress { 351 | display: inline-block; /* 1 */ 352 | vertical-align: baseline; /* 2 */ 353 | } 354 | 355 | /** 356 | * Remove the default vertical scrollbar in IE. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; 361 | } 362 | 363 | /** 364 | * 1. Add the correct box sizing in IE 10-. 365 | * 2. Remove the padding in IE 10-. 366 | */ 367 | 368 | [type="checkbox"], 369 | [type="radio"] { 370 | box-sizing: border-box; /* 1 */ 371 | padding: 0; /* 2 */ 372 | } 373 | 374 | /** 375 | * Correct the cursor style of increment and decrement buttons in Chrome. 376 | */ 377 | 378 | [type="number"]::-webkit-inner-spin-button, 379 | [type="number"]::-webkit-outer-spin-button { 380 | height: auto; 381 | } 382 | 383 | /** 384 | * 1. Correct the odd appearance in Chrome and Safari. 385 | * 2. Correct the outline style in Safari. 386 | */ 387 | 388 | [type="search"] { 389 | -webkit-appearance: textfield; /* 1 */ 390 | outline-offset: -2px; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 395 | */ 396 | 397 | [type="search"]::-webkit-search-cancel-button, 398 | [type="search"]::-webkit-search-decoration { 399 | -webkit-appearance: none; 400 | } 401 | 402 | /** 403 | * 1. Correct the inability to style clickable types in iOS and Safari. 404 | * 2. Change font properties to `inherit` in Safari. 405 | */ 406 | 407 | ::-webkit-file-upload-button { 408 | -webkit-appearance: button; /* 1 */ 409 | font: inherit; /* 2 */ 410 | } 411 | 412 | /* Interactive 413 | ========================================================================== */ 414 | 415 | /* 416 | * Add the correct display in IE 9-. 417 | * 1. Add the correct display in Edge, IE, and Firefox. 418 | */ 419 | 420 | details, /* 1 */ 421 | menu { 422 | display: block; 423 | } 424 | 425 | /* 426 | * Add the correct display in all browsers. 427 | */ 428 | 429 | summary { 430 | display: list-item; 431 | } 432 | 433 | /* Scripting 434 | ========================================================================== */ 435 | 436 | /** 437 | * Add the correct display in IE 9-. 438 | */ 439 | 440 | canvas { 441 | display: inline-block; 442 | } 443 | 444 | /** 445 | * Add the correct display in IE. 446 | */ 447 | 448 | template { 449 | display: none; 450 | } 451 | 452 | /* Hidden 453 | ========================================================================== */ 454 | 455 | /** 456 | * Add the correct display in IE 10-. 457 | */ 458 | 459 | [hidden] { 460 | display: none; 461 | } 462 | -------------------------------------------------------------------------------- /tests/examples/static/pygments.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight { background: #ffffff; } 3 | .highlight .c { color: #aaaaaa; font-style: italic } /* Comment */ 4 | .highlight .err { color: #FF0000; background-color: #FFAAAA } /* Error */ 5 | .highlight .k { color: #0000aa } /* Keyword */ 6 | .highlight .ch { color: #aaaaaa; font-style: italic } /* Comment.Hashbang */ 7 | .highlight .cm { color: #aaaaaa; font-style: italic } /* Comment.Multiline */ 8 | .highlight .cp { color: #4c8317 } /* Comment.Preproc */ 9 | .highlight .cpf { color: #aaaaaa; font-style: italic } /* Comment.PreprocFile */ 10 | .highlight .c1 { color: #aaaaaa; font-style: italic } /* Comment.Single */ 11 | .highlight .cs { color: #0000aa; font-style: italic } /* Comment.Special */ 12 | .highlight .gd { color: #aa0000 } /* Generic.Deleted */ 13 | .highlight .ge { font-style: italic } /* Generic.Emph */ 14 | .highlight .gr { color: #aa0000 } /* Generic.Error */ 15 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 16 | .highlight .gi { color: #00aa00 } /* Generic.Inserted */ 17 | .highlight .go { color: #888888 } /* Generic.Output */ 18 | .highlight .gp { color: #555555 } /* Generic.Prompt */ 19 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 20 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 21 | .highlight .gt { color: #aa0000 } /* Generic.Traceback */ 22 | .highlight .kc { color: #0000aa } /* Keyword.Constant */ 23 | .highlight .kd { color: #0000aa } /* Keyword.Declaration */ 24 | .highlight .kn { color: #0000aa } /* Keyword.Namespace */ 25 | .highlight .kp { color: #0000aa } /* Keyword.Pseudo */ 26 | .highlight .kr { color: #0000aa } /* Keyword.Reserved */ 27 | .highlight .kt { color: #00aaaa } /* Keyword.Type */ 28 | .highlight .m { color: #009999 } /* Literal.Number */ 29 | .highlight .s { color: #aa5500 } /* Literal.String */ 30 | .highlight .na { color: #1e90ff } /* Name.Attribute */ 31 | .highlight .nb { color: #00aaaa } /* Name.Builtin */ 32 | .highlight .nc { color: #00aa00; text-decoration: underline } /* Name.Class */ 33 | .highlight .no { color: #aa0000 } /* Name.Constant */ 34 | .highlight .nd { color: #888888 } /* Name.Decorator */ 35 | .highlight .ni { color: #880000; font-weight: bold } /* Name.Entity */ 36 | .highlight .nf { color: #00aa00 } /* Name.Function */ 37 | .highlight .nn { color: #00aaaa; text-decoration: underline } /* Name.Namespace */ 38 | .highlight .nt { color: #1e90ff; font-weight: bold } /* Name.Tag */ 39 | .highlight .nv { color: #aa0000 } /* Name.Variable */ 40 | .highlight .ow { color: #0000aa } /* Operator.Word */ 41 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 42 | .highlight .mb { color: #009999 } /* Literal.Number.Bin */ 43 | .highlight .mf { color: #009999 } /* Literal.Number.Float */ 44 | .highlight .mh { color: #009999 } /* Literal.Number.Hex */ 45 | .highlight .mi { color: #009999 } /* Literal.Number.Integer */ 46 | .highlight .mo { color: #009999 } /* Literal.Number.Oct */ 47 | .highlight .sa { color: #aa5500 } /* Literal.String.Affix */ 48 | .highlight .sb { color: #aa5500 } /* Literal.String.Backtick */ 49 | .highlight .sc { color: #aa5500 } /* Literal.String.Char */ 50 | .highlight .dl { color: #aa5500 } /* Literal.String.Delimiter */ 51 | .highlight .sd { color: #aa5500 } /* Literal.String.Doc */ 52 | .highlight .s2 { color: #aa5500 } /* Literal.String.Double */ 53 | .highlight .se { color: #aa5500 } /* Literal.String.Escape */ 54 | .highlight .sh { color: #aa5500 } /* Literal.String.Heredoc */ 55 | .highlight .si { color: #aa5500 } /* Literal.String.Interpol */ 56 | .highlight .sx { color: #aa5500 } /* Literal.String.Other */ 57 | .highlight .sr { color: #009999 } /* Literal.String.Regex */ 58 | .highlight .s1 { color: #aa5500 } /* Literal.String.Single */ 59 | .highlight .ss { color: #0000aa } /* Literal.String.Symbol */ 60 | .highlight .bp { color: #00aaaa } /* Name.Builtin.Pseudo */ 61 | .highlight .fm { color: #00aa00 } /* Name.Function.Magic */ 62 | .highlight .vc { color: #aa0000 } /* Name.Variable.Class */ 63 | .highlight .vg { color: #aa0000 } /* Name.Variable.Global */ 64 | .highlight .vi { color: #aa0000 } /* Name.Variable.Instance */ 65 | .highlight .vm { color: #aa0000 } /* Name.Variable.Magic */ 66 | .highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /tests/examples/static/sakura.css: -------------------------------------------------------------------------------- 1 | /* Sakura.css v1.5.0 2 | * ================ 3 | * Minimal css theme. 4 | * Project: https://github.com/oxalorg/sakura/ 5 | */ 6 | /* Body */ 7 | html { 8 | font-size: 62.5%; 9 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; 10 | } 11 | 12 | body { 13 | font-size: 1.8rem; 14 | line-height: 1.618; 15 | max-width: 38em; 16 | margin: auto; 17 | color: #4a4a4a; 18 | background-color: #f9f9f9; 19 | padding: 13px; 20 | } 21 | 22 | @media (max-width: 684px) { 23 | body { 24 | font-size: 1.53rem; 25 | } 26 | } 27 | @media (max-width: 382px) { 28 | body { 29 | font-size: 1.35rem; 30 | } 31 | } 32 | h1, h2, h3, h4, h5, h6 { 33 | line-height: 1.1; 34 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; 35 | font-weight: 700; 36 | margin-top: 3rem; 37 | margin-bottom: 1.5rem; 38 | overflow-wrap: break-word; 39 | word-wrap: break-word; 40 | -ms-word-break: break-all; 41 | word-break: break-word; 42 | } 43 | 44 | h1 { 45 | font-size: 2.35em; 46 | } 47 | 48 | h2 { 49 | font-size: 2em; 50 | } 51 | 52 | h3 { 53 | font-size: 1.75em; 54 | } 55 | 56 | h4 { 57 | font-size: 1.5em; 58 | } 59 | 60 | h5 { 61 | font-size: 1.25em; 62 | } 63 | 64 | h6 { 65 | font-size: 1em; 66 | } 67 | 68 | p { 69 | margin-top: 0px; 70 | margin-bottom: 2.5rem; 71 | } 72 | 73 | small, sub, sup { 74 | font-size: 75%; 75 | } 76 | 77 | hr { 78 | border-color: #1d7484; 79 | } 80 | 81 | a { 82 | text-decoration: none; 83 | color: #1d7484; 84 | } 85 | a:visited { 86 | color: #144f5a; 87 | } 88 | a:hover { 89 | color: #982c61; 90 | border-bottom: 2px solid #4a4a4a; 91 | } 92 | 93 | ul { 94 | padding-left: 1.4em; 95 | margin-top: 0px; 96 | margin-bottom: 2.5rem; 97 | } 98 | 99 | li { 100 | margin-bottom: 0.4em; 101 | } 102 | 103 | blockquote { 104 | margin-left: 0px; 105 | margin-right: 0px; 106 | padding-left: 1em; 107 | padding-top: 0.8em; 108 | padding-bottom: 0.8em; 109 | padding-right: 0.8em; 110 | border-left: 5px solid #1d7484; 111 | margin-bottom: 2.5rem; 112 | background-color: #f1f1f1; 113 | } 114 | 115 | blockquote p { 116 | margin-bottom: 0; 117 | } 118 | 119 | img, video { 120 | height: auto; 121 | max-width: 100%; 122 | margin-top: 0px; 123 | margin-bottom: 2.5rem; 124 | } 125 | 126 | /* Pre and Code */ 127 | pre { 128 | background-color: #f1f1f1; 129 | display: block; 130 | padding: 1em; 131 | overflow-x: auto; 132 | margin-top: 0px; 133 | margin-bottom: 2.5rem; 134 | font-size: 0.9em; 135 | } 136 | 137 | code, kbd, samp { 138 | font-size: 0.9em; 139 | padding: 0 0.5em; 140 | background-color: #f1f1f1; 141 | white-space: pre-wrap; 142 | } 143 | 144 | pre > code { 145 | padding: 0; 146 | background-color: transparent; 147 | white-space: pre; 148 | font-size: 1em; 149 | } 150 | 151 | /* Tables */ 152 | table { 153 | text-align: justify; 154 | width: 100%; 155 | border-collapse: collapse; 156 | margin-bottom: 2rem; 157 | } 158 | 159 | td, th { 160 | padding: 0.5em; 161 | border-bottom: 1px solid #f1f1f1; 162 | } 163 | 164 | /* Buttons, forms and input */ 165 | input, textarea { 166 | border: 1px solid #4a4a4a; 167 | } 168 | input:focus, textarea:focus { 169 | border: 1px solid #1d7484; 170 | } 171 | 172 | textarea { 173 | width: 100%; 174 | } 175 | 176 | .button, button, input[type=submit], input[type=reset], input[type=button], input[type=file]::file-selector-button { 177 | display: inline-block; 178 | padding: 5px 10px; 179 | text-align: center; 180 | text-decoration: none; 181 | white-space: nowrap; 182 | background-color: #1d7484; 183 | color: #f9f9f9; 184 | border-radius: 1px; 185 | border: 1px solid #1d7484; 186 | cursor: pointer; 187 | box-sizing: border-box; 188 | } 189 | .button[disabled], button[disabled], input[type=submit][disabled], input[type=reset][disabled], input[type=button][disabled], input[type=file]::file-selector-button[disabled] { 190 | cursor: default; 191 | opacity: 0.5; 192 | } 193 | .button:hover, button:hover, input[type=submit]:hover, input[type=reset]:hover, input[type=button]:hover, input[type=file]::file-selector-button:hover { 194 | background-color: #982c61; 195 | color: #f9f9f9; 196 | outline: 0; 197 | } 198 | .button:focus-visible, button:focus-visible, input[type=submit]:focus-visible, input[type=reset]:focus-visible, input[type=button]:focus-visible, input[type=file]::file-selector-button:focus-visible { 199 | outline-style: solid; 200 | outline-width: 2px; 201 | } 202 | 203 | textarea, select, input { 204 | color: #4a4a4a; 205 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 206 | margin-bottom: 10px; 207 | background-color: #f1f1f1; 208 | border: 1px solid #f1f1f1; 209 | border-radius: 4px; 210 | box-shadow: none; 211 | box-sizing: border-box; 212 | } 213 | textarea:focus, select:focus, input:focus { 214 | border: 1px solid #1d7484; 215 | outline: 0; 216 | } 217 | 218 | input[type=checkbox]:focus { 219 | outline: 1px dotted #1d7484; 220 | } 221 | 222 | label, legend, fieldset { 223 | display: block; 224 | margin-bottom: 0.5rem; 225 | font-weight: 600; 226 | } 227 | -------------------------------------------------------------------------------- /tests/examples/templates/layout/base.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | {% if metadata %} 4 |32 | 33 | Posts 34 | 35 | | 36 | 37 | Tags 38 | 39 |
40 |