├── .gitignore ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── app.py ├── app_old.py ├── freeze.py ├── pages ├── contact.md ├── index.md └── team.md ├── static ├── base.css ├── images │ └── logo.png └── js │ └── dogs.js ├── templates └── page.html └── tutorial ├── build-settings.png ├── build-settings.webp ├── deploy-success.webp ├── deploys.webp ├── failed-logs.webp ├── failed.webp ├── jamstack.png └── jamstack.webp /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .DS_Store 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 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 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brantley Harris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | flask = "*" 10 | frozen-flask = "*" 11 | flask-flatpages = "*" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "460450319555230ccda61f116247e2ef1404e5c694d20a3eecbec1e737417c33" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "click": { 20 | "hashes": [ 21 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 22 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 23 | ], 24 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 25 | "version": "==7.1.2" 26 | }, 27 | "flask": { 28 | "hashes": [ 29 | "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", 30 | "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" 31 | ], 32 | "index": "pypi", 33 | "version": "==1.1.2" 34 | }, 35 | "flask-flatpages": { 36 | "hashes": [ 37 | "sha256:591506153c66e6b1d2286b5c757b652fb14d6b9ec8e55bd4e58d1799a0bea278", 38 | "sha256:67401f57fc3b2746cfac6ca6750cd199bdfce446fba5bda48a645d7938367317" 39 | ], 40 | "index": "pypi", 41 | "version": "==0.7.2" 42 | }, 43 | "frozen-flask": { 44 | "hashes": [ 45 | "sha256:0a7a71334210ce84f8cbd1dc23c8b265d3e21748805c09c77d0e6fbcc4faab14", 46 | "sha256:83858d6ed8b9d3fa7fc9523e415e65fb86b99352798d7695f63cffbd59a56269" 47 | ], 48 | "index": "pypi", 49 | "version": "==0.15" 50 | }, 51 | "importlib-metadata": { 52 | "hashes": [ 53 | "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", 54 | "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" 55 | ], 56 | "markers": "python_version < '3.8'", 57 | "version": "==1.7.0" 58 | }, 59 | "itsdangerous": { 60 | "hashes": [ 61 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 62 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 63 | ], 64 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 65 | "version": "==1.1.0" 66 | }, 67 | "jinja2": { 68 | "hashes": [ 69 | "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", 70 | "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" 71 | ], 72 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 73 | "version": "==2.11.2" 74 | }, 75 | "markdown": { 76 | "hashes": [ 77 | "sha256:1fafe3f1ecabfb514a5285fca634a53c1b32a81cb0feb154264d55bf2ff22c17", 78 | "sha256:c467cd6233885534bf0fe96e62e3cf46cfc1605112356c4f9981512b8174de59" 79 | ], 80 | "markers": "python_version >= '3.5'", 81 | "version": "==3.2.2" 82 | }, 83 | "markupsafe": { 84 | "hashes": [ 85 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 86 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 87 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 88 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 89 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 90 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 91 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 92 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 93 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 94 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 95 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 96 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 97 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 98 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 99 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 100 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 101 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 102 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 103 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 104 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 105 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 106 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 107 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 108 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 109 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 110 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 111 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 112 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 113 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 114 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 115 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 116 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 117 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" 118 | ], 119 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 120 | "version": "==1.1.1" 121 | }, 122 | "pyyaml": { 123 | "hashes": [ 124 | "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", 125 | "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", 126 | "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", 127 | "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", 128 | "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", 129 | "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", 130 | "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", 131 | "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", 132 | "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", 133 | "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", 134 | "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" 135 | ], 136 | "markers": "python_version != '3.4'", 137 | "version": "==5.3.1" 138 | }, 139 | "werkzeug": { 140 | "hashes": [ 141 | "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", 142 | "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" 143 | ], 144 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 145 | "version": "==1.0.1" 146 | }, 147 | "zipp": { 148 | "hashes": [ 149 | "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", 150 | "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" 151 | ], 152 | "markers": "python_version >= '3.6'", 153 | "version": "==3.1.0" 154 | } 155 | }, 156 | "develop": {} 157 | } 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Embracing JAMStack with Python: Generating a Static Website with Flask and Deploying to Netlify 2 | 3 | ### The Big Idea 4 | 5 | JAMStack completely changes how individuals and entire organizations alike iterate on a web app. It decouples your 6 | frontend and backend workflow, so you can focus on speed to your end-user, worry less about how 7 | it's served, keep separate iteration cycles, and best of all, enable easy feature branching. 8 | 9 | I'll show, step by step, how you can use the powerful, sophisticated tools of Flask and Python without 10 | bothering with specialized static-site generators. 11 | 12 | ### Contents: 13 | 14 | - [JAMStack: What it is and why it's awesome](#jamstack) 15 | - [Generating Static Websites with Flask: A step-by-step tutorial](#generating-static-websites-with-flask) 16 | - [Netlify: What it is and how to deploy your site](#netlify) 17 | - [Connecting a Bundler: Use with Rollup, Webpack, Parcel, etc](#connecting-a-bundler) 18 | - [Final Thoughts: Some tips and where to go from here](#final-thoughts) 19 | 20 | ### Important Links: 21 | 22 | - [Github Repository for this Tutorial](https://github.com/DeadWisdom/flask-static-tutorial) 23 | - [Project Example: Rare Pup Detective Agency](https://flask-static-tutorial.netlify.app/) 24 | - [Netlify](https://netlify.com) 25 | - [Flask](https://flask.palletsprojects.com/) 26 | 27 | ### Before We Begin 28 | 29 | - Black lives matter. 30 | - We need diversity and women in tech. 31 | - Trans rights are human rights. 32 | - Reminder that we are quickly destroying the earth. 33 | 34 | All I ask for this content is that you are mindful about applying technology. It is not ours to fix 35 | everything, nor must we take on every cause as an individual, but we are the modern day wizards and 36 | we must remember that our actions _matter_. 37 | 38 | And finally, if you struggle with this tutorial or the concepts herein, know that we all struggle. 39 | Tech is a deep, long journey, we all (at every level) get frustrated and doubt ourselves constantly. 40 | The best way is to keep going, ignore the haters, and reach out to others for help. 41 | 42 | # JAMStack 43 | 44 | JAMStack is a terrible name. It sounds like a local event for jarring preserves. But it's a great 45 | concept: Focus on a separation of Javascript, API, and Markup. 46 | 47 | The frontend and the backend decouple, with the Markup becoming a _static_ platform which the 48 | JavaScript builds on. Dynamic data is delivered from the API to the client via JavaScript. You can 49 | still do some dynamic Markup generation on the server, but that is now an edge-case and is done 50 | through the API. 51 | 52 | Since our client assets are now all static, it lends itself to serving directly from a Content 53 | Delivery Network. This ensures your site is delivered as fast and efficiently as possible. This is 54 | where Netlify comes in. It makes deploying from your repository to a website super slick. And a real 55 | fire and forget solution. More on that later. 56 | 57 | ![A diagram showing JAMStack elements. JavaScript and Markup are static. JavaScript enhances Markup. API is dynamic. API and JavaScript send data to each other.](https://flask-static-tutorial.netlify.app/static/jamstack.webp) 58 | 59 | Keeping your frontend and backend decoupled is an amazing thing once you set it up. It's easily 60 | worth the price of admission. Backend often requires a lot of tests, analysis, optimization, and 61 | generally way longer iteration cycles than frontend, which often wants to make changes and iterate 62 | by the minute with immediate results. When you are changing the border radius of a button, you'd rather 63 | not wait for a backend build that's often compiling a docker image, running tests, deploying, etc... 64 | 65 | Testing frontend feature branches, without needing to create a whole new backend environment is game 66 | changing. It revolutionizes everyone's experience, from the developer all the way to business 67 | stakeholders. Netlify gives you a unique URL _for every deploy_ meaning you can try out new 68 | features, review old deploys, and quickly test for production readiness. 69 | 70 | Lastly, keeping things decoupled also makes it easy to slot in serverless endpoints or backendless 71 | options like Firebase or AWS Amplify. 72 | 73 | Modularity... Composition... Wow! Who knew it was so great? 74 | 75 | ### When not to use JAMStack 76 | 77 | JAMStack has one place where classic markup generation is better, and that's when you need to 78 | generate a lot of _dynamic content_, i.e. Markup that changes depending on the user viewing or some 79 | other trip to the database. 80 | 81 | With something like a stock-ticker, that's still doable, because it comes from an API. But for 82 | something like a CMS, where the content often changes via a database, then classic Jinja rendering 83 | is still where it's at. 84 | 85 | Still, a lot of the promises of database-driven CMSs have yet to materialize, and people are 86 | increasingly finding it easier to simply change the content in the source code and redeploy, 87 | especially when it's done automatically and quickly. 88 | 89 | Further it's simple to put some content generation behind an API endpoint, if you don't need a lot 90 | of that. 91 | 92 | # Generating Static Websites with Flask 93 | 94 | This technique allows you to generate a static website in much the same way you'd make a classic 95 | Flask app. And this example parallels examples shown by generators like 11ty, Gatsby, and Jekyll, 96 | but in my opinion is better because it allows us to use Python, Flask and all the great tools that 97 | come with. 98 | 99 | Some benefits of using Flask instead of other static site generators: 100 | 101 | - During development, we just get to use a Flask server, there's no compile step. 102 | - No scaling problems when you get to lots of pages. 103 | - We can use the same tools and mindset we use to build standard servers and API servers. 104 | - No new domain-specific languages to do things like for-loops, and shoe-horn database querying into Markdown. 105 | - We can still integrate with any database Flask can, any remote api, and generally do anything Python can do, which is a lot. 106 | 107 | Now, we could easily use Django, FastAPI, Starlette, or any other framework for this, 108 | but Flask has two extensions that make the process really easy: Frozen-Flask and Flask-FlatPages. 109 | 110 | ## Tutorial 111 | 112 | We're going to break our endeavour into these goals: 113 | 114 | 1. [Setup / Install Our Dependencies and Setup Github](#goal-1-setup--install-our-dependencies-and-setup-github) 115 | 2. [Create Our Flask App](#goal-2-create-our-flask-app) 116 | 3. [Freeze It](#goal-3-freeze-it) 117 | 4. [Add Pages / Content with Markdown](#goal-4-add-pages--content-with-markdown) 118 | 5. [Add JavaScript and Connect to an API](#add-some-javascript-and-connect-to-an-api) 119 | 120 | Then afterwards, I'll show you how to deploy with Netilify. 121 | 122 | Now to it! 123 | 124 | ## Goal 1: Setup / Install our Dependencies and Setup Github 125 | 126 | First we will setup our Git repository. Using _Github_ here, cause it's a big old standard. 127 | 128 | Let's create [a new repository](https://github.com/new). I named it "flask-static-tutorial". I 129 | checked "Initialize this repository with a README", added a gitignore (Python), and a license (MIT). 130 | You do you. 131 | 132 | Once it's made, clone the repository locally. 133 | 134 | I'm going to assume you have Python 3.6+ on your system. 135 | I like using [Pyenv](https://github.com/pyenv/pyenv) which manages the installation and selection of 136 | multiple Python versions. A great tutorial [is available here](https://realpython.com/intro-to-pyenv/). 137 | 138 | We're also going to use [Pipenv](https://pipenv-fork.readthedocs.io/en/latest/) for this tutorial. 139 | It will manage our Python dependencies. I'm using it mostly because Netlify supports `Pipfile` directly. 140 | So go ahead and [install that](https://pipenv-fork.readthedocs.io/en/latest/install.html#installing-pipenv). 141 | 142 | Now that we have those installed, we'll install our requirements: 143 | 144 | ```bash 145 | $ pipenv --python 3.7 install flask frozen-flask flask-flatpages 146 | ``` 147 | 148 | Pipenv nicely creates a virtual environment for us, a `Pipfile`, and `Pipfile.Lock`, and installs our 149 | packages. We also tell it to use 3.7, because it is the default version for Netlify. 150 | 151 | Now let's commit it, and move on: 152 | 153 | ```bash 154 | $ git add . 155 | $ git commit -m 'project setup' 156 | $ git push 157 | ``` 158 | 159 | ## Goal 2: Create Our Flask App 160 | 161 | This part is basically just following the flask tutorial. The only little change is that we're using 162 | Pipenv. 163 | 164 | Let's make `app.py`: 165 | 166 | ```python 167 | from flask import Flask 168 | 169 | # Create our app object, use this page as our settings (will pick up DEBUG) 170 | app = Flask(__name__) 171 | 172 | # For settings, we just use this file itself, very easy to configure 173 | app.config.from_object(__name__) 174 | 175 | # We want Flask to allow no slashes after paths, because they get turned into flat files 176 | app.url_map.strict_slashes = False 177 | 178 | # Create a route to our index page at the root url, return a simple greeting 179 | @app.route("/") 180 | def index(): 181 | return "Hello, Flask" 182 | ``` 183 | 184 | Basic Flask stuff. Of course, you can do anything here, even talk to a database, but you don't want 185 | to add anything that is based on user interaction or state. Everything should respond 186 | to a simple GET request. Accessing the `request` global is a red-flag here. It's _static_ content after all. 187 | 188 | Now we run our server, and this is where using Flask is great, because we can develop our site as we 189 | go, without having to rebuild or run some command to do so after changes. We just pretend we are 190 | making any old Flask site. And really, we are. 191 | 192 | First setup our environment, then run it with Pipenv. 193 | 194 | ```bash 195 | $ export FLASK_DEBUG=True 196 | $ export FLASK_APP=app.py 197 | $ pipenv run flask run 198 | ``` 199 | 200 | We tell `pipenv` to `run flask` and `flask` to `run`. Hope you follow. Alternatively you can 201 | create a [pipenv script](https://pipenv-fork.readthedocs.io/en/latest/advanced.html#custom-script-shortcuts). 202 | But _[that's your bizzniss](https://twitter.com/iamtabithabrown)_. 203 | 204 | Now we can open our browser to [http://127.0.0.1:5000/](http://127.0.0.1:5000/) and be greeted. 205 | 206 | Great work! But don't get arrogant, we have more to do. 207 | 208 | ## Goal 3: Freeze It 209 | 210 | Did you know you can [make frozen ice cubes in the hot desert like the Persians thousands of years ago?](https://www.realclearscience.com/blog/2018/07/09/how_people_created_ice_in_the_desert_2000_years_ago.html) 211 | Did you also know you can make a frozen Flask website in your computer? Even in the desert. 212 | 213 | Earlier we installed [Frozen-Flask](https://pythonhosted.org/Frozen-Flask/). To get started, 214 | all we need is another file `freeze.py`: 215 | 216 | ```python 217 | from flask_frozen import Freezer 218 | from app import app 219 | 220 | freezer = Freezer(app) 221 | 222 | if __name__ == '__main__': 223 | freezer.freeze() 224 | ``` 225 | 226 | And then run it: 227 | 228 | ```bash 229 | $ pipenv run python freeze.py 230 | ``` 231 | 232 | You'll see that it makes a directory `build` and the file `index.html`. Open it up and you'll see 233 | exactly what your browser gets when it goes to 'http://127.0.0.1:5000/' when our flask process is 234 | running. 235 | 236 | Frozen-Flask is really simple. It just runs your app, gets every root endpoint (ones without a path 237 | variable), copies the Markup, and saves it to a corresponding file. To find other pages, it tracks 238 | every response from `url_for()` and adds them to its queue. 239 | 240 | To find pages that are outside of your root tree, [read more about how it finds urls here](https://pythonhosted.org/Frozen-Flask/#finding-urls). 241 | 242 | We are really close. Now we just need, you know content. 243 | 244 | ## Goal 4: Add Pages / Content with Markdown 245 | 246 | It is time. Time for you to look into your cold, frozen heart to see what kind of website you want 247 | to make. For me, it's simple: A site for a pet detective agency. But you do you. 248 | 249 | One of the things 11ty, Jekyll and basically every ~~blog creation framework~~ static site generator 250 | does is easily let you create pages with markdown. We're going to do the same. 251 | 252 | This is where Flask-FlatPages comes in. It lets us create pages in any format we want, process them, 253 | and then deliver them as HTML. 254 | 255 | ### App.py Changes 256 | 257 | Let's update our `app.py`: 258 | 259 | ```python 260 | from flask import Flask, render_template 261 | from flask_flatpages import FlatPages 262 | 263 | # Tell Flatpages to auto reload when a page is changed, and look for .md files 264 | FLATPAGES_AUTO_RELOAD = True 265 | FLATPAGES_EXTENSION = '.md' 266 | 267 | # Create our app object, use this page as our settings (will pick up DEBUG) 268 | app = Flask(__name__) 269 | 270 | # For settings, we just use this file itself, very easy to configure 271 | app.config.from_object(__name__) 272 | 273 | # We want Flask to allow no slashes after paths, because they get turned into flat files 274 | app.url_map.strict_slashes = False 275 | 276 | # Create an instance of our extension 277 | pages = FlatPages(app) 278 | 279 | # Route to FlatPages at our root, and route any path that ends in ".html" 280 | @app.route("/") 281 | @app.route("/.html") 282 | def page(path=None): 283 | # Look for the page with FlatPages, or find "index" if we have no path 284 | page = pages.get_or_404(path or 'index') 285 | 286 | # Render the template "page.html" with our page and title 287 | return render_template("page.html", page=page, title=page.meta['title']) 288 | ``` 289 | 290 | Note, that we could make other routes to do whatever we want, but currently we just have one that 291 | works with Flask-FlatPages. 292 | 293 | ### Markdown Pages 294 | 295 | We've got Flask-FlatPages looking for pages in a `pages` directory as default. So let's create some 296 | pages. You can click on each one to copy them or make your own content. Any file you add here that 297 | has an '.md' extension will turn into a page, provided you also link to it in another page with 298 | `url_for()`. 299 | 300 | [browse files](https://github.com/DeadWisdom/flask-static-tutorial/blob/master/pages/) 301 | 302 | pages/ 303 | content.md 304 | index.md 305 | team.md 306 | 307 | At the top of each page, you'll notice its "meta" section, which is YAML and looks something like 308 | this: 309 | 310 | ```yaml 311 | Title: Rare Pup Detective Agency 312 | Description: We sniff out the clues. 313 | ``` 314 | 315 | Within the pages, you might notice some HTML. Don't forget, markdown lets us embed HTML! Use it. 316 | 317 | ### Jinja Template 318 | 319 | Now let's create our Jinja template, that serves as the wrapper for our pages `templates/page.html`: 320 | 321 | ```html+jinja 322 | 323 | 324 | 325 | 326 | {{ title }} 327 | 328 | 329 | 330 | 331 |
332 | 338 |
339 | 340 |
341 |
342 | {% block content %} 343 |

{{ page.meta.description }}

344 | {{ page.html|safe }} 345 | {% endblock content %} 346 |
347 |
348 | 349 | 350 | ``` 351 | 352 | Mostly we have a basic page here. There are a few things to note: 353 | 354 | ```html+jinja 355 | Team 356 | ``` 357 | 358 | We use flask's `url_for()` method to get the final url of our "team" page, also we have add an 359 | 'active' attribute to the `` tag when we are on that page, for styling purposes. 360 | 361 | Also in our content block, we do: 362 | 363 | ```html+jinja 364 |

{{ page.meta.description }}

365 | {{ page.html|safe }} 366 | ``` 367 | 368 | We add an `

` with the description from the page meta. And finally, we grab the `page.html` and run 369 | a `safe` filter on it because it includes HTML that we want to render unescaped. 370 | 371 | ### Static assets 372 | 373 | Finally, any static assets like images, css, or js that we link to need to go into `/static`. 374 | Frozen-Flask will automatically grab them if they are used. For this example, I'm only using a few, 375 | but you might many more: 376 | 377 | [browse files](https://github.com/DeadWisdom/flask-static-tutorial/blob/master/static/) 378 | 379 | static/ 380 | images/ 381 | logo.png 382 | base.css 383 | 384 | ### Develop & Freeze 385 | 386 | Alright, alright. That was a lot. Let's check out how we did, make changes, etc. During development 387 | we simply use the flask server, and it will auto-reload. We can pretend it's just a normal flask 388 | site: 389 | 390 | ```bash 391 | $ pipenv run flask run 392 | ``` 393 | 394 | When we're done, we can test the freeze, it should put all our pages and assets into the build 395 | directory: 396 | 397 | build/ 398 | static/ 399 | images/ 400 | logo.png 401 | base.css 402 | content.html 403 | index.html 404 | team.html 405 | 406 | ## Goal 5: Add JavaScript and Connect to an API 407 | 408 | As a final step, we'll add some functionality to call an external API. In a real world 409 | scenario, you'd create the API, and host it yourself, or use a serverless option. Here we are just 410 | calling out to the wonderful service [Dog CEO, Dog Api](https://dog.ceo/dog-api/). 411 | 412 | We'll add the script linked below: 413 | 414 | [static/js/dogs.js](https://github.com/DeadWisdom/flask-static-tutorial/blob/master/static/js/dogs.js) 415 | 416 | It defines a custom element "dog-picture" which we've already spread through the site. If the user 417 | didn't have JavaScript, the browser just ignores all of the `` elements. Once this 418 | script is loaded, they spring to life. Custom Elements. Neat. 419 | 420 | We link it in `templates/page.html` with a simple: 421 | 422 | ```html 423 | 424 | ``` 425 | 426 | When we freeze, you'll see, it gets added to `build/`. If we need to compile our JavaScript with 427 | rollup, parcel, or webpack it's simple enough to run that before we do freeze. 428 | 429 | Only one more thing, let's deploy the thing... 430 | 431 | # Netlify 432 | 433 | Once we have the static content, we need somewhere to put it. Since it's all static, we can 434 | basically put it anywhere: behind nginx, on an S3, whatever. But it's best to put it behind a Content 435 | Delivery Network, where your data will live on multiple servers around the globe that are specially 436 | designed to cache and serve content quickly. The only trouble is managing that deployment process. 437 | 438 | And that's why we use Netlify. They have really figured out the process. Basically, it hooks into 439 | your repo, listens for updates, then runs a build command, puts it on a CDN, and routes to it. 440 | 441 | Every build gets served by a _unique url_. This is key. It means we can create feature branches of our frontend without a thought. 442 | 443 | And, nicely, it's free for small projects like this. 444 | 445 | ## Create the Site 446 | 447 | First we make our account: [https://app.netlify.com/](https://app.netlify.com/) 448 | 449 | Next we make our first site: [https://app.netlify.com/start](https://app.netlify.com/start) 450 | 451 | Connect it to Github, and select your repository. For the build options, 452 | tell it the command "python freeze.py", and our build directory is "build". 453 | 454 | Note: Netlify's basic build environment will look at our `Pipfile` to determine the python version, 455 | and requirements. Since it configures the base python environment we don't have to actually run it 456 | with pipenv. 457 | 458 | ![Screenshot of the "Basic build settings" section of the Netlify New Site options](https://flask-static-tutorial.netlify.app/static/build-settings.webp) 459 | 460 | Now press "Deploy Site". You can watch the progress by clicking on the deploy item, it will give you a full output 461 | and status. If you get a big old "Deploy Failed", go into the deploy and scroll down in the page 462 | to see why. 463 | 464 | ![Screenshot of the logs for a failed deploy.](https://flask-static-tutorial.netlify.app/static/failed-logs.webp) 465 | 466 | When it succeeds you will see "Published" at at the top of the 'Deploys' screen you'll see a link 467 | like "https://infallible-beaver-cf7576.netlify.app". Click on it to view it. 468 | 469 | ![Screenshot of the deploy page for a successful deploy.](https://flask-static-tutorial.netlify.app/static/deploy-success.webp) 470 | 471 | As awesome as "infallible-beaver-cf7576" is, you can rename your site by going to 472 | Settings > Domain Management. You can also bring in a custom domain. 473 | 474 | Now every time you commit, it will go live. You can also make it only publish a specific branch, 475 | setup triggers, etc. There's a million different options, so play around. 476 | 477 | ## Some Netlify Extras 478 | 479 | Netlify has a decent redirect feature, allowing you to remap a request like /api/\* to wherever you want, including a custom API endpoint. [Read about Redirects and Rewrites](https://docs.netlify.com/routing/redirects/). 480 | 481 | Our `contact.md` page has a form that doesn't go anywhere, but it has a `data-netlify="true"` tag, which is automatically processed by Netlify. It gather all submissions, and you can read them in the site admin. Also, you can set up notifications for them. [Read about Netlify Forms](https://docs.netlify.com/forms/setup/). 482 | 483 | Light authentication can be done pretty simply with [Netlify's Authentication System](https://docs.netlify.com/visitor-access/identity/). 484 | 485 | # Connecting a Bundler 486 | 487 | The great part about this approach is we don't need a build step, but sometimes you need JavaScript 488 | to bundle, by running Rollup, Webpack, Parcel, etc. There are two places you need this. One is 489 | during development when you make a change, and the other is during your publish step. 490 | 491 | ### Bundle For Publishing 492 | 493 | One is to change your Netlify build command to something like this: 494 | 495 | ```bash 496 | $ npm run build && python freeze.py 497 | ``` 498 | 499 | Have your package manager build right into your static folder like `/static/build`, and then have 500 | your Flask template pickup the JavaScript file there. Flask-Freeze will then move it to the `/build` 501 | directory for Netlify to use. 502 | 503 | ### Bundle For Development 504 | 505 | I'll encourage you to use a resource like [open-wc.org](https://open-wc.org) or [Snowpack](https://www.snowpack.dev/) so that you _don't_ have to build anything for development. ES6 being what it is, you don't need to 506 | anymore. 507 | 508 | That said, sometimes we don't get to choose. In that case, we need to run our flask server and the 509 | bundler in watch mode. 510 | 511 | The simplest way is to just run two terminals: 512 | 513 | ```bash 514 | Terminal 1: $ pipenv run flask run 515 | ``` 516 | 517 | ```bash 518 | Terminal 2: $ npm run webpack --watch 519 | ``` 520 | 521 | ### Bundle with Webpack 522 | 523 | If you're using Webpack, Andrew Montalenti ([@amontalenti](https://twitter.com/amontalenti)) wrote 524 | a bridge between Webpack and Flask that leans on Flask-Static, check it out at his gist: 525 | 526 | [https://gist.github.com/amontalenti/ffeca0dce10f29d42a82e80773804355](https://gist.github.com/amontalenti/ffeca0dce10f29d42a82e80773804355) 527 | 528 | # Final Thoughts 529 | 530 | I hope I've shown how JAMStack can be an amazing way to develop, and how we don't need to leave all 531 | the glorious wealth we have from Python to get there. From here you can look into [Zappa for 532 | serverless endpoints](https://github.com/Miserlou/Zappa), deploying containers to [AWS Fargate](https://aws.amazon.com/fargate/), or using good old [Google App Engine](https://cloud.google.com/appengine/), for your 533 | API needs. I still prefer the latter for setting up quick Python APIs. 534 | 535 | ## Some Tips 536 | 537 | Do remember that the JavaScript part of JAMStack should be your last resort. Too much these days 538 | developers are pushing giant React bundles into clients, and our poor devices just can't handle 539 | them. 540 | 541 | Progressive enhancement is still, after all these years, the goal. Think as close to Markup as 542 | possible, and then move outward as needed. So much of our time is spent implementing doomed features 543 | that are overly complex and JavaScript nightmares. Meanwhile what the user really needs is just some 544 | web form. JAMStack can help us here, but it can also lead us to Single Page React App nightmares. 545 | 546 | A great way to organize your apps is authenticated vs unauthenticated. Think of Markup as always 547 | anonymous, and then JavaScript adds functionality when they login. And think of your APIs as 548 | private and public endpoints. That helps you a lot when you get to caching. 549 | 550 | ## Alternatives to Netlify 551 | 552 | Netlify isn't really doing anything special here. There's nothing that necessitates it, and the adventurous might want to make their own deployment system. Static assets mean that you can put the hash in a filename and cache them indefinitely. Before Netlify I had a system that chunked my large JS bundles and placed them on S3 named by hash. When I made changes, clients only had to download the updated chunk files. 553 | 554 | You can also publish right to [GitHub Pages](https://pages.github.com/). 555 | 556 | Host it on an [NGinx](https://www.nginx.com/) server, and put [CloudFare](https://www.cloudflare.com/) in front of it. 557 | 558 | ## Alternatives to Flask 559 | 560 | Django has [Django-Freeze](https://github.com/fabiocaccamo/django-freeze) to create static content in 561 | much the same way you'd do it here. 562 | 563 | [Pelican](https://blog.getpelican.com/) is a python based static site generator. 564 | 565 | Use [11ty](https://11ty.dev) if you want a full-featured specialized static generator written in 566 | Javascript. 567 | 568 | ## Communicate With Me! 569 | 570 | If you spot any problems, have any questions, or want to request further tutorials create an issue 571 | in the project repo: [https://github.com/DeadWisdom/flask-static-tutorial/issues](https://github.com/DeadWisdom/flask-static-tutorial/issues) 572 | 573 | Or hit me up on twitter: [@deadwisdom](https://twitter.com/deadwisdom) 574 | 575 | Also I am available as a consultant, primarily in helping organizations streamline their innovation 576 | and development cycles, especially where product, design, and development need to communicate. 577 | 578 | Thanks to those that helped me writing this, including [@matteasterday](https://twitter.com/matteasterday), [@DanielReesLewis](https://twitter.com/DanielReesLewis), and [@amontalenti](https://twitter.com/amontalenti). 579 | 580 | And thank you! 581 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | from flask_flatpages import FlatPages 3 | 4 | # Tell Flatpages to auto reload when a page is changed, and look for .md files 5 | FLATPAGES_AUTO_RELOAD = True 6 | FLATPAGES_EXTENSION = '.md' 7 | 8 | # Create our app object, use this page as our settings (will pick up DEBUG) 9 | app = Flask(__name__) 10 | 11 | # For settings, we just use this file itself, very easy to configure 12 | app.config.from_object(__name__) 13 | 14 | # We want Flask to allow no slashes after paths, because they get turned into flat files 15 | app.url_map.strict_slashes = False 16 | 17 | # Create an instance of our extension 18 | pages = FlatPages(app) 19 | 20 | # Route to FlatPages at our root, and route any path that ends in ".html" 21 | @app.route("/") 22 | @app.route("/.html") 23 | def page(path=None): 24 | # Look for the page with FlatPages, or find "index" if we have no path 25 | page = pages.get_or_404(path or 'index') 26 | 27 | # Render the template "page.html" with our page and title 28 | return render_template("page.html", page=page, title=page.meta.get('title', '')) -------------------------------------------------------------------------------- /app_old.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | # Create our app object, use this page as our settings (will pick up DEBUG) 4 | app = Flask(__name__) 5 | 6 | # For our debug, we just use this file itself, easy to configure 7 | app.config.from_object(__name__) 8 | 9 | # We want Flask to allow no slashes after paths, because they get turned into flat files 10 | app.url_map.strict_slashes = False 11 | 12 | # Create a route to our index page at the root url, render the template "index.html" 13 | @app.route("/") 14 | def index(): 15 | return "Hello, Flask" -------------------------------------------------------------------------------- /freeze.py: -------------------------------------------------------------------------------- 1 | from flask_frozen import Freezer 2 | from app import app 3 | 4 | freezer = Freezer(app) 5 | 6 | if __name__ == '__main__': 7 | freezer.freeze() -------------------------------------------------------------------------------- /pages/contact.md: -------------------------------------------------------------------------------- 1 | title: Rare Pup Detective Agency - Contact Us 2 | description: We'd love to smell you! 3 | 4 | Any and all questions should be directed through the form below, thank you and have a zoomie day! 5 | 6 |
7 |

8 | 9 |

10 |

11 | 12 |

13 |

14 | 15 |

16 |

17 | 18 |

19 |
20 | -------------------------------------------------------------------------------- /pages/index.md: -------------------------------------------------------------------------------- 1 | title: Rare Pup Detective Agency 2 | description: We sniff out the clues 3 | 4 | The Rare Pup Detective Agency, Inc is a full-service private investigation agency based in 5 | Chicago. We provide a professional service that is as confidential as *paws*able. 6 | 7 | 8 | 9 | It was founded in 1998 by Merlin, certified Good Boy and mentor to all the organization's gumshoes. 10 | 11 | We specialize in: 12 | 13 | - Finding missing food items that have fallen on the floor. 14 | - Fetching sticks, balls, and other thrown items. 15 | - Identifying urine traces. 16 | - Surveillance of back yards. 17 | - Being good boys and girls. 18 | - The Reid Technique of interviewing, interrogation, and extraction. 19 | -------------------------------------------------------------------------------- /pages/team.md: -------------------------------------------------------------------------------- 1 | title: Rare Pup Detective Agency - Team 2 | description: All Certified Good Girl or Boy 3 | 4 | Name: Elmer 5 | Rank: Goodest Boy 6 | From: Los Angeles, CA 7 | Specialties: Dinner, Naps, Tug-of-War 8 | 9 | 10 | 11 |
12 | 13 | Name: Gina Bo Bina 14 | Rank: Goodest Girl 15 | From: Chicago, IL 16 | Specialties: Zoomies, Barking, Babies 17 | 18 | 19 | 20 |
21 | 22 | Name: Jackenheimer 23 | Rank: Gooder Boy 24 | From: Toledo, OH 25 | Specialties: Fetch, Pointing, Swimming 26 | 27 | 28 | 29 |
30 | 31 | Name: Coco 32 | Rank: Gooder Girl 33 | From: New York, NY 34 | Specialties: Naps, Naps, Naps 35 | 36 | 37 | 38 |
39 | 40 | Name: Millie 41 | Rank: Good Girl 42 | From: Elgin, IL 43 | Specialties: Waiting, Listening, Squinting Eyes 44 | 45 | 46 | 47 |
48 | 49 | Name: Mr. Bigglesworth 50 | Rank: Good Boy 51 | From: Rockford, IL 52 | Specialties: Grooming, Parading 53 | 54 | 55 | -------------------------------------------------------------------------------- /static/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-display: "Helvetica Neue", Helvetica, sans-serif; 3 | --font-paragraph: "Helvetica Neue", Helvetica, sans-serif; 4 | 5 | --color-light: #f5f5f5; 6 | --color-dark: #444; 7 | --color-action: #40a8f5; 8 | 9 | --max-width: 800px; 10 | } 11 | 12 | /* Root Elements */ 13 | html { 14 | font-size: 16px; 15 | font-family: var(--font-paragraph); 16 | } 17 | 18 | @media screen and (min-width: 320px) { 19 | html { 20 | font-size: calc(16px + 6 * ((100vw - 320px) / 680)); 21 | } 22 | } 23 | 24 | @media screen and (min-width: 1000px) { 25 | html { 26 | font-size: 22px; 27 | } 28 | } 29 | 30 | body { 31 | padding: 0; 32 | margin: 0; 33 | background-color: var(--color-light); 34 | color: var(--color-dark); 35 | } 36 | 37 | /* Typograph / Paragraph */ 38 | a { 39 | color: var(--color-action); 40 | } 41 | 42 | a:visited { 43 | opacity: 0.8; 44 | } 45 | 46 | h1 { 47 | font-size: 1.5em; 48 | text-transform: uppercase; 49 | font-weight: normal; 50 | } 51 | 52 | /* Layout */ 53 | body { 54 | color: var(--color-light); 55 | background-color: var(--color-dark); 56 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='540' height='450' viewBox='0 0 1080 900'%3E%3Cg fill-opacity='0.02'%3E%3Cpolygon fill='%23444' points='90 150 0 300 180 300'/%3E%3Cpolygon points='90 150 180 0 0 0'/%3E%3Cpolygon fill='%23AAA' points='270 150 360 0 180 0'/%3E%3Cpolygon fill='%23DDD' points='450 150 360 300 540 300'/%3E%3Cpolygon fill='%23999' points='450 150 540 0 360 0'/%3E%3Cpolygon points='630 150 540 300 720 300'/%3E%3Cpolygon fill='%23DDD' points='630 150 720 0 540 0'/%3E%3Cpolygon fill='%23444' points='810 150 720 300 900 300'/%3E%3Cpolygon fill='%23FFF' points='810 150 900 0 720 0'/%3E%3Cpolygon fill='%23DDD' points='990 150 900 300 1080 300'/%3E%3Cpolygon fill='%23444' points='990 150 1080 0 900 0'/%3E%3Cpolygon fill='%23DDD' points='90 450 0 600 180 600'/%3E%3Cpolygon points='90 450 180 300 0 300'/%3E%3Cpolygon fill='%23666' points='270 450 180 600 360 600'/%3E%3Cpolygon fill='%23AAA' points='270 450 360 300 180 300'/%3E%3Cpolygon fill='%23DDD' points='450 450 360 600 540 600'/%3E%3Cpolygon fill='%23999' points='450 450 540 300 360 300'/%3E%3Cpolygon fill='%23999' points='630 450 540 600 720 600'/%3E%3Cpolygon fill='%23FFF' points='630 450 720 300 540 300'/%3E%3Cpolygon points='810 450 720 600 900 600'/%3E%3Cpolygon fill='%23DDD' points='810 450 900 300 720 300'/%3E%3Cpolygon fill='%23AAA' points='990 450 900 600 1080 600'/%3E%3Cpolygon fill='%23444' points='990 450 1080 300 900 300'/%3E%3Cpolygon fill='%23222' points='90 750 0 900 180 900'/%3E%3Cpolygon points='270 750 180 900 360 900'/%3E%3Cpolygon fill='%23DDD' points='270 750 360 600 180 600'/%3E%3Cpolygon points='450 750 540 600 360 600'/%3E%3Cpolygon points='630 750 540 900 720 900'/%3E%3Cpolygon fill='%23444' points='630 750 720 600 540 600'/%3E%3Cpolygon fill='%23AAA' points='810 750 720 900 900 900'/%3E%3Cpolygon fill='%23666' points='810 750 900 600 720 600'/%3E%3Cpolygon fill='%23999' points='990 750 900 900 1080 900'/%3E%3Cpolygon fill='%23999' points='180 0 90 150 270 150'/%3E%3Cpolygon fill='%23444' points='360 0 270 150 450 150'/%3E%3Cpolygon fill='%23FFF' points='540 0 450 150 630 150'/%3E%3Cpolygon points='900 0 810 150 990 150'/%3E%3Cpolygon fill='%23222' points='0 300 -90 450 90 450'/%3E%3Cpolygon fill='%23FFF' points='0 300 90 150 -90 150'/%3E%3Cpolygon fill='%23FFF' points='180 300 90 450 270 450'/%3E%3Cpolygon fill='%23666' points='180 300 270 150 90 150'/%3E%3Cpolygon fill='%23222' points='360 300 270 450 450 450'/%3E%3Cpolygon fill='%23FFF' points='360 300 450 150 270 150'/%3E%3Cpolygon fill='%23444' points='540 300 450 450 630 450'/%3E%3Cpolygon fill='%23222' points='540 300 630 150 450 150'/%3E%3Cpolygon fill='%23AAA' points='720 300 630 450 810 450'/%3E%3Cpolygon fill='%23666' points='720 300 810 150 630 150'/%3E%3Cpolygon fill='%23FFF' points='900 300 810 450 990 450'/%3E%3Cpolygon fill='%23999' points='900 300 990 150 810 150'/%3E%3Cpolygon points='0 600 -90 750 90 750'/%3E%3Cpolygon fill='%23666' points='0 600 90 450 -90 450'/%3E%3Cpolygon fill='%23AAA' points='180 600 90 750 270 750'/%3E%3Cpolygon fill='%23444' points='180 600 270 450 90 450'/%3E%3Cpolygon fill='%23444' points='360 600 270 750 450 750'/%3E%3Cpolygon fill='%23999' points='360 600 450 450 270 450'/%3E%3Cpolygon fill='%23666' points='540 600 630 450 450 450'/%3E%3Cpolygon fill='%23222' points='720 600 630 750 810 750'/%3E%3Cpolygon fill='%23FFF' points='900 600 810 750 990 750'/%3E%3Cpolygon fill='%23222' points='900 600 990 450 810 450'/%3E%3Cpolygon fill='%23DDD' points='0 900 90 750 -90 750'/%3E%3Cpolygon fill='%23444' points='180 900 270 750 90 750'/%3E%3Cpolygon fill='%23FFF' points='360 900 450 750 270 750'/%3E%3Cpolygon fill='%23AAA' points='540 900 630 750 450 750'/%3E%3Cpolygon fill='%23FFF' points='720 900 810 750 630 750'/%3E%3Cpolygon fill='%23222' points='900 900 990 750 810 750'/%3E%3Cpolygon fill='%23222' points='1080 300 990 450 1170 450'/%3E%3Cpolygon fill='%23FFF' points='1080 300 1170 150 990 150'/%3E%3Cpolygon points='1080 600 990 750 1170 750'/%3E%3Cpolygon fill='%23666' points='1080 600 1170 450 990 450'/%3E%3Cpolygon fill='%23DDD' points='1080 900 1170 750 990 750'/%3E%3C/g%3E%3C/svg%3E"); 57 | } 58 | 59 | nav { 60 | max-width: var(--max-width); 61 | margin: 0 auto; 62 | padding: 3vh 0; 63 | } 64 | 65 | nav a { 66 | text-decoration: none; 67 | color: var(--color-light); 68 | margin: 0 1ch; 69 | } 70 | 71 | nav a:hover { 72 | text-decoration: underline; 73 | color: var(--color-action); 74 | } 75 | 76 | nav a[active] { 77 | opacity: 0.5; 78 | text-decoration: none !important; 79 | cursor: default; 80 | } 81 | 82 | main { 83 | background-color: var(--color-light); 84 | color: var(--color-dark); 85 | } 86 | 87 | article { 88 | max-width: var(--max-width); 89 | margin: 0 auto; 90 | padding: 1rem; 91 | } 92 | 93 | footer { 94 | font-size: 0.7rem; 95 | padding: 2ch; 96 | } 97 | 98 | footer > * { 99 | max-width: var(--max-width); 100 | margin-left: auto; 101 | margin-right: auto; 102 | } 103 | 104 | /* Components */ 105 | nav .logo { 106 | height: 140px; 107 | background: no-repeat url(images/logo.png); 108 | background-size: contain; 109 | width: 140px; 110 | vertical-align: middle; 111 | display: inline-block; 112 | } 113 | 114 | dog-picture img { 115 | height: 300px; 116 | object-fit: contain; 117 | } 118 | 119 | /* Form */ 120 | form { 121 | margin-left: auto; 122 | margin-right: auto; 123 | } 124 | 125 | form .field label { 126 | font-size: 0.8em; 127 | } 128 | 129 | form .field input, 130 | form .field textarea { 131 | display: block; 132 | border-radius: 0; 133 | background-color: #ffffff; 134 | font-family: inherit; 135 | border-style: solid; 136 | border-width: 1px; 137 | border-color: #cccccc; 138 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); 139 | color: rgba(0, 0, 0, 0.75); 140 | display: block; 141 | font-size: 0.875rem; 142 | margin: 0 0 1rem 0; 143 | padding: 0.5rem; 144 | height: 2.3125rem; 145 | width: 100%; 146 | -webkit-box-sizing: border-box; 147 | -moz-box-sizing: border-box; 148 | box-sizing: border-box; 149 | transition: box-shadow 0.45s, border-color 0.45s ease-in-out; 150 | } 151 | 152 | form .field textarea { 153 | display: block; 154 | height: 10.3125rem; 155 | } 156 | 157 | form button { 158 | padding: 0.5rem; 159 | border-style: solid; 160 | border-width: 1px; 161 | border-radius: 0; 162 | } 163 | -------------------------------------------------------------------------------- /static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeadWisdom/flask-static-tutorial/608c3a3a1371525f30b77ecde256f9c1e12d707a/static/images/logo.png -------------------------------------------------------------------------------- /static/js/dogs.js: -------------------------------------------------------------------------------- 1 | export class DogPicture extends HTMLElement { 2 | constructor() { 3 | super(); 4 | this.img = document.createElement("img"); 5 | this.appendChild(this.img); 6 | } 7 | 8 | connectedCallback() { 9 | this.breed = this.getAttribute("breed") || null; 10 | this.loadRandomImage(this.breed); 11 | } 12 | 13 | loadRandomImage(breed) { 14 | let url = "https://dog.ceo/api/breeds/image/random"; 15 | if (breed) { 16 | url = 17 | "https://dog.ceo/api/breed/" + breed.toLowerCase() + "/images/random"; 18 | } 19 | 20 | fetch(url) 21 | .then((response) => response.json()) 22 | .then((data) => { 23 | this.img.setAttribute("src", data.message); 24 | if (breed) { 25 | this.img.setAttribute("alt", "picture of a cute " + breed + " dog"); 26 | } else { 27 | this.img.setAttribute("alt", "picture of a cute dog"); 28 | } 29 | }); 30 | } 31 | } 32 | 33 | customElements.define("dog-picture", DogPicture); 34 | -------------------------------------------------------------------------------- /templates/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | 8 | 9 | 10 | 11 |
12 | 18 |
19 | 20 |
21 |
22 | {% block content %} 23 |

{{ page.meta.description }}

24 | {{ page.html|safe }} 25 | {% endblock content %} 26 |
27 |
28 | 29 | 36 | 37 | -------------------------------------------------------------------------------- /tutorial/build-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeadWisdom/flask-static-tutorial/608c3a3a1371525f30b77ecde256f9c1e12d707a/tutorial/build-settings.png -------------------------------------------------------------------------------- /tutorial/build-settings.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeadWisdom/flask-static-tutorial/608c3a3a1371525f30b77ecde256f9c1e12d707a/tutorial/build-settings.webp -------------------------------------------------------------------------------- /tutorial/deploy-success.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeadWisdom/flask-static-tutorial/608c3a3a1371525f30b77ecde256f9c1e12d707a/tutorial/deploy-success.webp -------------------------------------------------------------------------------- /tutorial/deploys.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeadWisdom/flask-static-tutorial/608c3a3a1371525f30b77ecde256f9c1e12d707a/tutorial/deploys.webp -------------------------------------------------------------------------------- /tutorial/failed-logs.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeadWisdom/flask-static-tutorial/608c3a3a1371525f30b77ecde256f9c1e12d707a/tutorial/failed-logs.webp -------------------------------------------------------------------------------- /tutorial/failed.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeadWisdom/flask-static-tutorial/608c3a3a1371525f30b77ecde256f9c1e12d707a/tutorial/failed.webp -------------------------------------------------------------------------------- /tutorial/jamstack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeadWisdom/flask-static-tutorial/608c3a3a1371525f30b77ecde256f9c1e12d707a/tutorial/jamstack.png -------------------------------------------------------------------------------- /tutorial/jamstack.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeadWisdom/flask-static-tutorial/608c3a3a1371525f30b77ecde256f9c1e12d707a/tutorial/jamstack.webp --------------------------------------------------------------------------------