├── .circleci └── config.yml ├── .gitignore ├── .standard.yml ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── example ├── .browserslistrc ├── .env ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── images │ │ │ └── .keep │ │ └── stylesheets │ │ │ └── application.css │ ├── channels │ │ └── application_cable │ │ │ ├── channel.rb │ │ │ └── connection.rb │ ├── controllers │ │ ├── application_controller.rb │ │ ├── concerns │ │ │ └── .keep │ │ └── docs │ │ │ └── api │ │ │ └── houses_controller.rb │ ├── helpers │ │ └── application_helper.rb │ ├── javascript │ │ ├── channels │ │ │ ├── consumer.js │ │ │ └── index.js │ │ └── packs │ │ │ └── application.js │ ├── jobs │ │ └── application_job.rb │ ├── mailers │ │ └── application_mailer.rb │ ├── models │ │ ├── application_record.rb │ │ └── concerns │ │ │ └── .keep │ └── views │ │ └── layouts │ │ ├── application.html.erb │ │ ├── mailer.html.erb │ │ └── mailer.text.erb ├── babel.config.js ├── bin │ ├── bundle │ ├── rails │ ├── rake │ ├── setup │ ├── webpack │ ├── webpack-dev-server │ └── yarn ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── cable.yml │ ├── credentials.yml.enc │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── application_controller_renderer.rb │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── content_security_policy.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── static.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ ├── spring.rb │ ├── storage.yml │ ├── webpack │ │ ├── development.js │ │ ├── environment.js │ │ ├── production.js │ │ └── test.js │ └── webpacker.yml ├── cypress.json ├── cypress │ ├── integration │ │ └── static-rails.js │ ├── plugins │ │ └── index.js │ └── support │ │ ├── commands.js │ │ └── index.js ├── db │ └── seeds.rb ├── lib │ ├── assets │ │ └── .keep │ └── tasks │ │ └── .keep ├── log │ └── .keep ├── package.json ├── postcss.config.js ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ ├── favicon.ico │ ├── index.html │ └── robots.txt ├── script │ └── test ├── static │ ├── blog-docs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package-lock.json │ │ └── package.json │ ├── blog │ │ ├── .gitignore │ │ ├── archetypes │ │ │ └── default.md │ │ ├── config.toml │ │ ├── content │ │ │ └── posts │ │ │ │ └── my-first-post.md │ │ └── themes │ │ │ └── ultralight │ │ │ ├── archetypes │ │ │ └── default.md │ │ │ ├── assets │ │ │ ├── js │ │ │ │ └── index.js │ │ │ ├── sass │ │ │ │ ├── code.css │ │ │ │ ├── layout.sass │ │ │ │ ├── main.sass │ │ │ │ ├── readability.sass │ │ │ │ └── vars.sass │ │ │ └── vendor │ │ │ │ └── css │ │ │ │ └── modern-normalize.css │ │ │ ├── layouts │ │ │ ├── 404.html │ │ │ ├── _default │ │ │ │ ├── baseof.html │ │ │ │ ├── list.html │ │ │ │ └── single.html │ │ │ ├── index.html │ │ │ ├── index.rss.xml │ │ │ └── partials │ │ │ │ ├── css.html │ │ │ │ ├── footer.html │ │ │ │ ├── head.html │ │ │ │ ├── header.html │ │ │ │ ├── js.html │ │ │ │ ├── posts-list.html │ │ │ │ └── social-preview.html │ │ │ └── theme.toml │ ├── docs │ │ ├── .bundle │ │ │ └── config │ │ ├── .gitignore │ │ ├── 404.html │ │ ├── Gemfile │ │ ├── Gemfile.lock │ │ ├── _config.yml │ │ ├── _posts │ │ │ └── 2020-04-27-welcome-to-jekyll.markdown │ │ ├── about.markdown │ │ ├── assets │ │ │ ├── a_css.css │ │ │ ├── a_css.css.gz │ │ │ ├── a_html.html │ │ │ ├── a_html.html.gz │ │ │ ├── a_javascript.js │ │ │ ├── a_javascript.js.gz │ │ │ ├── a_json.json │ │ │ ├── a_json.json.gz │ │ │ ├── an_image.png │ │ │ └── an_image.png.gz │ │ └── index.markdown │ └── marketing │ │ ├── .gitignore │ │ ├── archetypes │ │ └── default.md │ │ ├── config.toml │ │ ├── content │ │ └── posts │ │ │ └── my-first-post.md │ │ ├── layouts │ │ └── 404.html │ │ └── themes │ │ └── ultralight │ │ ├── archetypes │ │ └── default.md │ │ ├── assets │ │ ├── js │ │ │ └── index.js │ │ ├── sass │ │ │ ├── code.css │ │ │ ├── layout.sass │ │ │ ├── main.sass │ │ │ ├── readability.sass │ │ │ └── vars.sass │ │ └── vendor │ │ │ └── css │ │ │ └── modern-normalize.css │ │ ├── layouts │ │ ├── 404.html │ │ ├── _default │ │ │ ├── baseof.html │ │ │ ├── list.html │ │ │ └── single.html │ │ ├── index.html │ │ ├── index.rss.xml │ │ └── partials │ │ │ ├── css.html │ │ │ ├── footer.html │ │ │ ├── head.html │ │ │ ├── header.html │ │ │ ├── js.html │ │ │ ├── posts-list.html │ │ │ └── social-preview.html │ │ └── theme.toml ├── storage │ └── .keep ├── test │ ├── application_system_test_case.rb │ ├── channels │ │ └── application_cable │ │ │ └── connection_test.rb │ ├── controllers │ │ └── .keep │ ├── fixtures │ │ ├── .keep │ │ └── files │ │ │ └── .keep │ ├── helpers │ │ └── .keep │ ├── integration │ │ └── .keep │ ├── mailers │ │ └── .keep │ ├── models │ │ └── .keep │ ├── system │ │ └── .keep │ └── test_helper.rb ├── tmp │ └── .keep ├── vendor │ └── .keep └── yarn.lock ├── lib ├── generators │ ├── static_rails │ │ └── initializer_generator.rb │ └── templates │ │ └── static.rb ├── static-rails.rb ├── static-rails │ ├── compile.rb │ ├── configuration.rb │ ├── determines_whether_to_handle_request.rb │ ├── error.rb │ ├── file_handler.rb │ ├── gets_csrf_token.rb │ ├── matches_request_to_static_site.rb │ ├── proxy_middleware.rb │ ├── rack_server_check.rb │ ├── railtie.rb │ ├── request_forgery_protection_fallback.rb │ ├── server.rb │ ├── server_store.rb │ ├── site.rb │ ├── site_middleware.rb │ ├── site_plus_csrf_middleware.rb │ ├── static_middleware.rb │ ├── validates_csrf_token.rb │ ├── version.rb │ └── waits_for_connection.rb └── tasks │ └── static-rails.rake ├── script ├── setup └── test ├── static-rails.gemspec └── test └── test_helper.rb /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/ruby:2.7-node-browsers 6 | environment: 7 | RUBYOPT: "-W:no-deprecated -W:no-experimental" 8 | steps: 9 | - checkout 10 | 11 | - run: echo 127.0.0.1 blog.localhost | sudo tee -a /etc/hosts 12 | 13 | # One of the apps needs hugo 14 | - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends hugo 15 | 16 | # Make sure we use the right Bundler 17 | - run: gem install bundler --version `tail -1 Gemfile.lock` 18 | 19 | # Bundle install dependencies 20 | - type: cache-restore 21 | key: v1-main-{{ checksum "Gemfile.lock" }} 22 | 23 | # Bundle install dependencies for example app 24 | - type: cache-restore 25 | key: v1-example-{{ checksum "example/Gemfile.lock" }} 26 | 27 | # Yarn dependencies 28 | - restore_cache: 29 | keys: 30 | - v2-yarn-{{ checksum "example/yarn.lock" }} 31 | # fallback to using the latest cache if no exact match is found 32 | - v2-yarn- 33 | 34 | # Bundle install dependencies for jekyll app 35 | - type: cache-restore 36 | key: v1-jekyll-{{ checksum "example/static/docs/Gemfile.lock" }} 37 | 38 | # Npm install for Eleventy app 39 | - restore_cache: 40 | keys: 41 | - v1-eleventy-{{ checksum "example/static/blog-docs/package-lock.json" }} 42 | # fallback to using the latest cache if no exact match is found 43 | - v1-eleventy- 44 | 45 | - run: ./script/setup 46 | 47 | - type: cache-save 48 | key: v1-main-{{ checksum "Gemfile.lock" }} 49 | paths: 50 | - vendor/bundle 51 | 52 | - type: cache-save 53 | key: v1-example-{{ checksum "example/Gemfile.lock" }} 54 | paths: 55 | - example/vendor/bundle 56 | 57 | - save_cache: 58 | paths: 59 | - example/node_modules 60 | - ~/.cache 61 | key: v2-yarn-{{ checksum "example/yarn.lock" }} 62 | 63 | - type: cache-save 64 | key: v1-jekyll-{{ checksum "example/static/docs/Gemfile.lock" }} 65 | paths: 66 | - example/static/docs/vendor/bundle 67 | 68 | - save_cache: 69 | paths: 70 | - example/static/blog-docs/node_modules 71 | key: v1-eleventy-{{ checksum "example/static/blog-docs/package-lock.json" }} 72 | 73 | - run: ./script/test 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /vendor/bundle 10 | /example/vendor/bundle 11 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | ruby_version: 2.5 2 | ignore: 3 | - example/**/* 4 | - node_modules/**/* 5 | - vendor/**/* 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | cache: bundler 4 | rvm: 5 | - 2.7.0 6 | before_install: gem install bundler -v 2.1.4 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 2 | 3 | * Add support for Rails 6.1 when CSRF is enabled 4 | * Increase required Ruby to 2.5 5 | 6 | ## 0.0.14 7 | 8 | * HTML & XML won't be cached by default in production 9 | [#20](https://github.com/testdouble/static-rails/pull/20) 10 | 11 | ## 0.0.13 12 | 13 | * 404 pages served in production via a site's `compile_404_file_path` setting 14 | will now also send the HTTP status code of 404 instead of 200 15 | 16 | ## 0.0.12 17 | 18 | * Fix an issue in which enabling force_ssl would result in redirects to the 19 | obfuscated `/_static_rails/` path. Resolved this by placing the static-rails 20 | middleware after `ActionDispatch::SSL`. Note that this will break if you 21 | remove `Rack::SendFile` from your app's middleware stack 22 | 23 | ## 0.0.11 24 | 25 | * Inline the `ActionDispatch::FileHandler` from Rails master so that we can 26 | target a single stable version of its API and control what MIME types it 27 | considers to be compressible (bonus is that it effectively backports brotli 28 | compression to pre-6.1 rails apps) 29 | 30 | ## 0.0.10 31 | 32 | * Change default `cache-control` header for static assets being served from disk 33 | from `no-cache` to `"public; max-age=31536000"` 34 | 35 | ## 0.0.9 36 | 37 | * When using CSRF protection, the artificial path info will now be 38 | "__static_rails__" instead of a random string, to make logs appear cleaner 39 | * Attempt to guard against future internal changes to Rails' request forgery 40 | protection by adding `method_missing` that calls through 41 | 42 | ## 0.0.8 43 | 44 | * Add support for the [CSRF 45 | changes](https://github.com/rails/rails/commit/358ff18975f26e820ea355ec113ffc5228e59af8) in Rails 6.0.3.1 46 | 47 | ## 0.0.7 48 | 49 | * Ensure that CSRF tokens are valid, at the cost of some performance and 50 | reliance on additional Rails internals. As a result CSRF cookie setting is now 51 | disabled by default [#6](https://github.com/testdouble/static-rails/pull/6) 52 | 53 | ## 0.0.6 54 | 55 | * Fix an issue where `ActionDispatch::FileHandler` won't be loaded in the event 56 | that static-rails is serving compiled assets but Rails is not 57 | 58 | ## 0.0.5 59 | 60 | * Add a site option `compile_404_file_path` to specify a file to be used as a 61 | 404 page when serving compiled assets and no file is found 62 | 63 | ## 0.0.4 64 | 65 | * Add a cookie named `_csrf_token` by default to all static site requests, so 66 | that your static sites can make CSRF-protected requests of your server 67 | ([#4](https://github.com/testdouble/static-rails/pull/4)) 68 | 69 | ## 0.0.3 70 | 71 | * Add `url_skip_paths_starting_with` array of strings option to site 72 | configuration. Will fall through to the next matching site or all the way to 73 | the Rails app. 74 | 75 | ## 0.0.2 76 | 77 | * Add `env` hash option to site configuration 78 | 79 | ## 0.0.1 80 | 81 | * Initial release 82 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake", "~> 13.0" 6 | gem "minitest", "~> 5.0" 7 | gem "standard" 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | static-rails (0.1.0) 5 | rack-proxy (~> 0.6) 6 | railties (>= 5.0.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actionpack (6.1.4) 12 | actionview (= 6.1.4) 13 | activesupport (= 6.1.4) 14 | rack (~> 2.0, >= 2.0.9) 15 | rack-test (>= 0.6.3) 16 | rails-dom-testing (~> 2.0) 17 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 18 | actionview (6.1.4) 19 | activesupport (= 6.1.4) 20 | builder (~> 3.1) 21 | erubi (~> 1.4) 22 | rails-dom-testing (~> 2.0) 23 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 24 | activesupport (6.1.4) 25 | concurrent-ruby (~> 1.0, >= 1.0.2) 26 | i18n (>= 1.6, < 2) 27 | minitest (>= 5.1) 28 | tzinfo (~> 2.0) 29 | zeitwerk (~> 2.3) 30 | ast (2.4.2) 31 | builder (3.2.4) 32 | concurrent-ruby (1.1.9) 33 | crass (1.0.6) 34 | erubi (1.10.0) 35 | i18n (1.8.10) 36 | concurrent-ruby (~> 1.0) 37 | loofah (2.10.0) 38 | crass (~> 1.0.2) 39 | nokogiri (>= 1.5.9) 40 | method_source (1.0.0) 41 | mini_portile2 (2.5.3) 42 | minitest (5.14.4) 43 | nokogiri (1.11.7) 44 | mini_portile2 (~> 2.5.0) 45 | racc (~> 1.4) 46 | parallel (1.20.1) 47 | parser (3.0.2.0) 48 | ast (~> 2.4.1) 49 | racc (1.5.2) 50 | rack (2.2.3) 51 | rack-proxy (0.7.0) 52 | rack 53 | rack-test (1.1.0) 54 | rack (>= 1.0, < 3) 55 | rails-dom-testing (2.0.3) 56 | activesupport (>= 4.2.0) 57 | nokogiri (>= 1.6) 58 | rails-html-sanitizer (1.3.0) 59 | loofah (~> 2.3) 60 | railties (6.1.4) 61 | actionpack (= 6.1.4) 62 | activesupport (= 6.1.4) 63 | method_source 64 | rake (>= 0.13) 65 | thor (~> 1.0) 66 | rainbow (3.0.0) 67 | rake (13.0.6) 68 | regexp_parser (2.1.1) 69 | rexml (3.2.5) 70 | rubocop (1.18.3) 71 | parallel (~> 1.10) 72 | parser (>= 3.0.0.0) 73 | rainbow (>= 2.2.2, < 4.0) 74 | regexp_parser (>= 1.8, < 3.0) 75 | rexml 76 | rubocop-ast (>= 1.7.0, < 2.0) 77 | ruby-progressbar (~> 1.7) 78 | unicode-display_width (>= 1.4.0, < 3.0) 79 | rubocop-ast (1.8.0) 80 | parser (>= 3.0.1.1) 81 | rubocop-performance (1.11.4) 82 | rubocop (>= 1.7.0, < 2.0) 83 | rubocop-ast (>= 0.4.0) 84 | ruby-progressbar (1.11.0) 85 | standard (1.1.5) 86 | rubocop (= 1.18.3) 87 | rubocop-performance (= 1.11.4) 88 | thor (1.1.0) 89 | tzinfo (2.0.4) 90 | concurrent-ruby (~> 1.0) 91 | unicode-display_width (2.0.0) 92 | zeitwerk (2.4.2) 93 | 94 | PLATFORMS 95 | ruby 96 | 97 | DEPENDENCIES 98 | minitest (~> 5.0) 99 | rake (~> 13.0) 100 | standard 101 | static-rails! 102 | 103 | BUNDLED WITH 104 | 2.2.15 105 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Test Double, LLC 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # static-rails 2 | 3 | [![CircleCI](https://circleci.com/gh/testdouble/static-rails.svg?style=svg)](https://circleci.com/gh/testdouble/static-rails) 4 | 5 | ## Build and serve your static sites from your Rails app 6 | 7 | **tl;dr in development, static-rails runs your static site generators & 8 | proxies requests; in production, it compiles and serves the final assets** 9 | 10 | Static site generators are hot right now. Maybe you're hip with "the 11 | [Jamstack](https://jamstack.org)", or maybe your API documentation is generated 12 | by [Hugo](https://gohugo.io), or maybe your marketing folks use 13 | [Jekyll](https://jekyllrb.com) for the company blog. 14 | 15 | Up until now, compiling static assets with any degree of sophistication beyond 16 | dumping them in your app's `public/` directory represented a significant 17 | deviation from the "Rails Way". But the alternative—keeping your static sites 18 | wholly separate from your Rails app—raises myriad operational challenges, from 19 | tracking multiple git repositories, to managing multiple server configurations, 20 | to figuring out a way to share common JavaScript and CSS assets between them 21 | all. 22 | 23 | No longer! static-rails lets you use your static asset generators of choice 24 | without forcing you to abandon your monolithic Rails architecture. 25 | 26 | Here's what it does: 27 | 28 | * In development, static-rails launches your sites' local servers and then 29 | proxies any requests to wherever you've mounted them in your Rails app so you 30 | can start a single server and transition work between your static sites and 31 | Rails app seamlessly 32 | 33 | * When deploying, static-rails will compile all your static assets when `rake 34 | assets:precompile` is run, meaning your assets will be built automatically 35 | when pushed to a platform like Heroku 36 | 37 | * In production, static-rails will serve your sites' compiled assets from disk 38 | with a similar features and performance to what you're familiar with if you've 39 | ever hosted files out of your `public/` directory 40 | 41 | ## Install 42 | 43 | Add this to your Gemfile: 44 | 45 | ``` 46 | gem "static-rails" 47 | ``` 48 | 49 | Then run this generator to create a configuration file 50 | `config/initializers/static.rb`: 51 | 52 | ``` 53 | $ rails g static_rails:initializer 54 | ``` 55 | 56 | You can check out the configuration options in the [generated file's 57 | comments](). 58 | 59 | Want an example of setting things up? You're in luck, there's an [example 60 | app](/example) right in this repo! 61 | 62 | ## Configuring the gem 63 | 64 | **(Want to dive right in? The generated initializer [enumerates every 65 | option](/lib/generators/templates/static.rb) and the [example app's 66 | config](https://github.com/testdouble/static-rails/blob/main/example/config/initializers/static.rb) 67 | sets up 4 sites.)** 68 | 69 | ### Top-level configuration 70 | 71 | So, what should you stick in your initializer's `StaticRails.config do |config|` 72 | block? These options are set right off the `config` object and control the 73 | overall behavior of the gem itself, across all your static sites: 74 | 75 | * **config.proxy_requests** (Default: `!Rails.env.production?`) when true, 76 | the gem's middleware requests that match where you've mounted your static site 77 | and proxy them to the development server 78 | 79 | * **config.serve_compiled_assets** (Default: `Rails.env.production?`) when true, 80 | the gem's middleware will find your static assets on disk and serve them using 81 | the same code that Rails uses to serve files out of `public/` 82 | 83 | * **config.ping_server_timeout** (Default: `5`) the number of seconds that (when 84 | `proxy_requests` is true, that the gem will wait for a response from a static 85 | site's server on any given request before timing out and raising an error 86 | 87 | * **config.set_csrf_token_cookie** (Default: `false`) when true, the gem's 88 | middleware will set a cookie named `_csrf_token` with each request of your 89 | static site. You can use this to set the `'x-csrf-token'` header on any 90 | requests from your site back to routes hosted by the Rails app that are 91 | [protected from CSRF 92 | forgery](https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf) 93 | (if you're not using Rails' cookie store for sessions or you're okay with API 94 | calls bypassing Rails CSRF, leave this off) 95 | 96 | ### Configuring your static sites themselves 97 | 98 | To tell the gem about your static sites, assign an array of hashes as `sites` 99 | (e.g. `config.sites = [{…}]`). Each of those hashes have the following options: 100 | 101 | * **name** (Required) A unique name for the site (primarily used for 102 | logging) 103 | 104 | * **source_dir** (Required) The file path (relative to the Rails app's root) to 105 | the static site's project directory 106 | 107 | * **url_subdomain** (Default: `nil`) Constrains the static site's assets to only 108 | be served for requests to a given subdomain (e.g. for a Rails app hosting 109 | `example.com`, a Hugo site at `blog.example.com` would set this to `"blog"`) 110 | 111 | * **url_root_path** (Default: `/`) The base URL path at which to mount the 112 | static site (e.g. if you want your Jekyll site hosted at `example.com/docs`, 113 | you'd set this to `/docs`). For most static site generators, you'll want to 114 | configure it to serve assets from the same path so links and other references 115 | are correct (see below for examples) 116 | 117 | * **url_skip_paths_starting_with** (Default: `[]`) If you want to mount your 118 | static site to `/` but allow the Rails app to serve APIs from `/api`, you can 119 | set the path prefix `["/api"]` here to tell the gem's middleware not to try to 120 | proxy or serve the request from your static site, but rather let Rails handle 121 | it 122 | 123 | * **start_server** (Default `!Rails.env.production?) When true, the gem will 124 | start the site's server (and if it ever exits, restart it) as your Rails app 125 | is booting up. All output from the server will be forwarded to STDOUT/STDERR 126 | 127 | * **server_command** (Required if `start_server` is true) the command to run to 128 | start the site's server, from the working directory of `source_dir` (e.g. 129 | `hugo server`) 130 | 131 | * **ping_server** (Default: true) if this and `start_server` are both true, then 132 | wait to proxy any requests until the server is accepting TCP connections 133 | 134 | * **env** (Default: `{}`) any environment variables you need to pass to either 135 | the server or compile commands (e.g. `env: {"BUNDLE_PATH" => 136 | "vendor/bundle"}`). Note that this configuration file is Ruby, so if you need 137 | to provide different env vars based on Rails environment, you have the power 138 | to do that! 139 | 140 | * **server_host** (Default: `localhost`) the host your static site's server will 141 | run on 142 | 143 | * **server_port** (Required if `proxy_requests` is true) the port your static 144 | site's server will accept requests on 145 | 146 | * **server_path** (Default: `"/"`) the root URL path to which requests should be 147 | proxied 148 | 149 | * **compile_comand** (Required) the command to be run by both the 150 | `static:compile` and `assets:precompile` Rake commands (e.g. `npm run build`), 151 | with working directory set to the site's `source_dir` 152 | 153 | * **compile_dir** (Required when `serve_compiled_assets` is true) the root file 154 | path to which production assets are compiled, relative to the site's 155 | `source_dir` 156 | 157 | * **compile_404_file_path** (Optional) When `serve_compiled_assets` is true, 158 | this file (relative to the `compile_dir`) will be served whenever the 159 | request's path does not match a file on disk 160 | 161 | ## Configuring your static site generators 162 | 163 | Assuming you won't be mounting your static site to your app's root `/` path, 164 | you'll probably need to configure its base URL path somehow. Here are 165 | some tips (and if your tool of choice isn't listed, we'd love a [pull 166 | request](https://github.com/testdouble/static-rails/edit/main/README.md)!): 167 | 168 | ### Using Jekyll 169 | 170 | If you have a Jekyll app that you plan on serving from a non-root path (say, 171 | `/docs`), then you'll need to set the `baseurl` in `_config.yml`: 172 | 173 | ```yml 174 | baseurl: "/docs" 175 | ``` 176 | 177 | (Note that this means running the Jekyll application directly using `bundle exec 178 | jekyll serve` will also start serving at `http://127.0.0.1:4000/docs/`) 179 | 180 | ### Using Hugo 181 | 182 | If you are mounting your [Hugo](https://gohugo.io) app to anything but the root 183 | path, you'll need to specify that path in the `baseURL` of your root 184 | `config.toml` file, like so: 185 | 186 | ```toml 187 | baseURL = "http://blog.example.com/docs" 188 | ``` 189 | 190 | Additionally, getting Hugo to play nicely when being proxied by your Rails 191 | server in development and test can be a little tricky, because most themes will 192 | render fully-qualified URLs into markup when running the `hugo server` command. 193 | That means if you're forwarding `http://blog.localhost:3000/docs` to a Hugo 194 | server running on `http://localhost:1313`, it's very likely the static files 195 | (e.g. all the links on the page) will have references to 196 | `http://localhost:1313`, which may result in accidentally navigating away from 197 | your Rails development server unexpectedly. 198 | 199 | To mitigate this, there are a few things you can do: 200 | 201 | * Favor `.RelPermalink` in your templates over `.Permalink` where possible. 202 | * In place of referring to `{{.Site.BaseURL}}` in your templates, generate a 203 | base path with `{{ "/" | relURL }}` (given the above `baseURL`, this will 204 | render `"/marketing/"`) 205 | 206 | Also, because Hugo will serve `/livereload.js` from the root, live-reloading probably 207 | won't work in development when running through the static-rails proxy. 208 | You might consider disabling it with `--disableLiveReload` unless you're serving 209 | Hugo from a root path ("`/`"). 210 | 211 | A static-rails config for a Hugo configuration in `sites` might look like: 212 | 213 | ```rb 214 | { 215 | name: "docs", 216 | url_root_path: "/docs", 217 | source_dir: "static/docs", 218 | server_command: "hugo server", 219 | server_port: 8080, 220 | compile_command: "hugo", 221 | compile_dir: "static/docs/public" 222 | } 223 | ``` 224 | 225 | ### Using Eleventy 226 | 227 | If you are mounting your [Eleventy](https://www.11ty.dev) app to anything but 228 | the root path, you'll want to configure a path prefix in `.eleventy.js` 229 | 230 | ```js 231 | module.exports = { 232 | pathPrefix: "/docs/" 233 | } 234 | ``` 235 | 236 | Alternatively, you can specify this from the command line with `--pathprefix 237 | /docs`. 238 | 239 | A static-rails config for an Eleventy configuration in `sites` might look like: 240 | 241 | ```rb 242 | { 243 | name: "docs", 244 | url_root_path: "/docs", 245 | source_dir: "static/docs", 246 | server_command: "npx @11ty/eleventy --serve --pathprefix /docs", 247 | server_port: 8080, 248 | compile_command: "npx @11ty/eleventy --pathprefix /docs", 249 | compile_dir: "static/docs/_site" 250 | } 251 | ``` 252 | 253 | ### Using Gatsby 254 | 255 | ⚠️ Gatsby is unlikely to work in development mode, due to [this 256 | issue](https://github.com/gatsbyjs/gatsby/issues/18143), wherein all the assets 257 | are actually served over a socket.io WebSocket channel and not able to be 258 | proxied effectively. ⚠️ 259 | 260 | If you're mounting a [Gatsby](https://www.gatsbyjs.org) site to a non-root path 261 | (e.g. in static-rails, you've configured its `url_root_path` to, say, 262 | `careers`), then you'll want to configure the same root path in Gatsby as well, 263 | so that its development servers and built assets line up correctly. 264 | 265 | To do this, first add the `pathPrefix` property to `gatsby-config.js`: 266 | 267 | ```js 268 | module.exports = { 269 | pathPrefix: `/careers`, 270 | // … 271 | } 272 | ``` 273 | 274 | Next, add the flag `--prefix-paths` to both the Gatsby site's `server_command` 275 | and `compile_command`, or else the setting will be ignored. 276 | 277 | A static-rails config for a Gatsby configuration in `sites` might look like: 278 | 279 | ```rb 280 | { 281 | name: "gatsby", 282 | url_root_path: "/docs", 283 | source_dir: "static/docs", 284 | server_command: "npx gatsby develop --prefix-paths", 285 | server_port: 8000, 286 | compile_command: "npx gatsby build --prefix-paths", 287 | compile_dir: "static/docs/public" 288 | }, 289 | ``` 290 | 291 | ## Code of Conduct 292 | 293 | This project follows Test Double's [code of 294 | conduct](https://testdouble.com/code-of-conduct) for all community interactions, 295 | including (but not limited to) one-on-one communications, public posts/comments, 296 | code reviews, pull requests, and GitHub issues. If violations occur, Test Double 297 | will take any action they deem appropriate for the infraction, up to and 298 | including blocking a user from the organization's repositories. 299 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | require "standard/rake" 4 | 5 | Rake::TestTask.new(:test) do |t| 6 | t.libs << "test" 7 | t.libs << "lib" 8 | t.test_files = FileList["test/**/*_test.rb"] 9 | end 10 | 11 | task default: ["standard:fix", :test] 12 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "rails/static/site" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /example/.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | -------------------------------------------------------------------------------- /example/.env: -------------------------------------------------------------------------------- 1 | CYPRESS_RAILS_PORT=3009 2 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | /db/*.sqlite3-* 14 | 15 | # Ignore all logfiles and tempfiles. 16 | /log/* 17 | /tmp/* 18 | !/log/.keep 19 | !/tmp/.keep 20 | 21 | # Ignore uploaded files in development. 22 | /storage/* 23 | !/storage/.keep 24 | 25 | /public/assets 26 | .byebug_history 27 | 28 | # Ignore master key for decrypting credentials and more. 29 | /config/master.key 30 | 31 | /public/packs 32 | /public/packs-test 33 | /node_modules 34 | /yarn-error.log 35 | yarn-debug.log* 36 | .yarn-integrity 37 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | ruby "~> 2.7.0" 5 | 6 | gem "rails" 7 | gem "sqlite3" 8 | gem "puma" 9 | gem "sass-rails" 10 | gem "webpacker" 11 | gem "turbolinks" 12 | gem "jbuilder" 13 | 14 | gem "standard" 15 | gem "static-rails", path: ".." 16 | gem "cypress-rails" 17 | gem "dotenv-rails" 18 | 19 | gem "bootsnap", require: false 20 | 21 | group :development, :test do 22 | gem "byebug", platforms: [:mri, :mingw, :x64_mingw] 23 | gem "pry-rails" 24 | end 25 | 26 | group :development do 27 | gem "web-console" 28 | gem "listen" 29 | end 30 | 31 | group :test do 32 | gem "capybara" 33 | gem "selenium-webdriver" 34 | gem "webdrivers" 35 | end 36 | 37 | gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] 38 | -------------------------------------------------------------------------------- /example/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | static-rails (0.1.0) 5 | rack-proxy (~> 0.6) 6 | railties (>= 5.0.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actioncable (6.1.4) 12 | actionpack (= 6.1.4) 13 | activesupport (= 6.1.4) 14 | nio4r (~> 2.0) 15 | websocket-driver (>= 0.6.1) 16 | actionmailbox (6.1.4) 17 | actionpack (= 6.1.4) 18 | activejob (= 6.1.4) 19 | activerecord (= 6.1.4) 20 | activestorage (= 6.1.4) 21 | activesupport (= 6.1.4) 22 | mail (>= 2.7.1) 23 | actionmailer (6.1.4) 24 | actionpack (= 6.1.4) 25 | actionview (= 6.1.4) 26 | activejob (= 6.1.4) 27 | activesupport (= 6.1.4) 28 | mail (~> 2.5, >= 2.5.4) 29 | rails-dom-testing (~> 2.0) 30 | actionpack (6.1.4) 31 | actionview (= 6.1.4) 32 | activesupport (= 6.1.4) 33 | rack (~> 2.0, >= 2.0.9) 34 | rack-test (>= 0.6.3) 35 | rails-dom-testing (~> 2.0) 36 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 37 | actiontext (6.1.4) 38 | actionpack (= 6.1.4) 39 | activerecord (= 6.1.4) 40 | activestorage (= 6.1.4) 41 | activesupport (= 6.1.4) 42 | nokogiri (>= 1.8.5) 43 | actionview (6.1.4) 44 | activesupport (= 6.1.4) 45 | builder (~> 3.1) 46 | erubi (~> 1.4) 47 | rails-dom-testing (~> 2.0) 48 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 49 | activejob (6.1.4) 50 | activesupport (= 6.1.4) 51 | globalid (>= 0.3.6) 52 | activemodel (6.1.4) 53 | activesupport (= 6.1.4) 54 | activerecord (6.1.4) 55 | activemodel (= 6.1.4) 56 | activesupport (= 6.1.4) 57 | activestorage (6.1.4) 58 | actionpack (= 6.1.4) 59 | activejob (= 6.1.4) 60 | activerecord (= 6.1.4) 61 | activesupport (= 6.1.4) 62 | marcel (~> 1.0.0) 63 | mini_mime (>= 1.1.0) 64 | activesupport (6.1.4) 65 | concurrent-ruby (~> 1.0, >= 1.0.2) 66 | i18n (>= 1.6, < 2) 67 | minitest (>= 5.1) 68 | tzinfo (~> 2.0) 69 | zeitwerk (~> 2.3) 70 | addressable (2.8.0) 71 | public_suffix (>= 2.0.2, < 5.0) 72 | ast (2.4.2) 73 | bindex (0.8.1) 74 | bootsnap (1.7.6) 75 | msgpack (~> 1.0) 76 | builder (3.2.4) 77 | byebug (11.1.3) 78 | capybara (3.35.3) 79 | addressable 80 | mini_mime (>= 0.1.3) 81 | nokogiri (~> 1.8) 82 | rack (>= 1.6.0) 83 | rack-test (>= 0.6.3) 84 | regexp_parser (>= 1.5, < 3.0) 85 | xpath (~> 3.2) 86 | childprocess (3.0.0) 87 | coderay (1.1.3) 88 | concurrent-ruby (1.1.9) 89 | crass (1.0.6) 90 | cypress-rails (0.5.1) 91 | puma (>= 3.8.0) 92 | railties (>= 5.2.0) 93 | dotenv (2.7.6) 94 | dotenv-rails (2.7.6) 95 | dotenv (= 2.7.6) 96 | railties (>= 3.2) 97 | erubi (1.10.0) 98 | ffi (1.15.3) 99 | globalid (0.4.2) 100 | activesupport (>= 4.2.0) 101 | i18n (1.8.10) 102 | concurrent-ruby (~> 1.0) 103 | jbuilder (2.11.2) 104 | activesupport (>= 5.0.0) 105 | listen (3.6.0) 106 | rb-fsevent (~> 0.10, >= 0.10.3) 107 | rb-inotify (~> 0.9, >= 0.9.10) 108 | loofah (2.10.0) 109 | crass (~> 1.0.2) 110 | nokogiri (>= 1.5.9) 111 | mail (2.7.1) 112 | mini_mime (>= 0.1.1) 113 | marcel (1.0.1) 114 | method_source (1.0.0) 115 | mini_mime (1.1.0) 116 | mini_portile2 (2.5.3) 117 | minitest (5.14.4) 118 | msgpack (1.4.2) 119 | nio4r (2.5.7) 120 | nokogiri (1.11.7) 121 | mini_portile2 (~> 2.5.0) 122 | racc (~> 1.4) 123 | parallel (1.20.1) 124 | parser (3.0.2.0) 125 | ast (~> 2.4.1) 126 | pry (0.14.1) 127 | coderay (~> 1.1) 128 | method_source (~> 1.0) 129 | pry-rails (0.3.9) 130 | pry (>= 0.10.4) 131 | public_suffix (4.0.6) 132 | puma (5.3.2) 133 | nio4r (~> 2.0) 134 | racc (1.5.2) 135 | rack (2.2.3) 136 | rack-proxy (0.7.0) 137 | rack 138 | rack-test (1.1.0) 139 | rack (>= 1.0, < 3) 140 | rails (6.1.4) 141 | actioncable (= 6.1.4) 142 | actionmailbox (= 6.1.4) 143 | actionmailer (= 6.1.4) 144 | actionpack (= 6.1.4) 145 | actiontext (= 6.1.4) 146 | actionview (= 6.1.4) 147 | activejob (= 6.1.4) 148 | activemodel (= 6.1.4) 149 | activerecord (= 6.1.4) 150 | activestorage (= 6.1.4) 151 | activesupport (= 6.1.4) 152 | bundler (>= 1.15.0) 153 | railties (= 6.1.4) 154 | sprockets-rails (>= 2.0.0) 155 | rails-dom-testing (2.0.3) 156 | activesupport (>= 4.2.0) 157 | nokogiri (>= 1.6) 158 | rails-html-sanitizer (1.3.0) 159 | loofah (~> 2.3) 160 | railties (6.1.4) 161 | actionpack (= 6.1.4) 162 | activesupport (= 6.1.4) 163 | method_source 164 | rake (>= 0.13) 165 | thor (~> 1.0) 166 | rainbow (3.0.0) 167 | rake (13.0.6) 168 | rb-fsevent (0.11.0) 169 | rb-inotify (0.10.1) 170 | ffi (~> 1.0) 171 | regexp_parser (2.1.1) 172 | rexml (3.2.5) 173 | rubocop (1.18.3) 174 | parallel (~> 1.10) 175 | parser (>= 3.0.0.0) 176 | rainbow (>= 2.2.2, < 4.0) 177 | regexp_parser (>= 1.8, < 3.0) 178 | rexml 179 | rubocop-ast (>= 1.7.0, < 2.0) 180 | ruby-progressbar (~> 1.7) 181 | unicode-display_width (>= 1.4.0, < 3.0) 182 | rubocop-ast (1.8.0) 183 | parser (>= 3.0.1.1) 184 | rubocop-performance (1.11.4) 185 | rubocop (>= 1.7.0, < 2.0) 186 | rubocop-ast (>= 0.4.0) 187 | ruby-progressbar (1.11.0) 188 | rubyzip (2.3.2) 189 | sass-rails (6.0.0) 190 | sassc-rails (~> 2.1, >= 2.1.1) 191 | sassc (2.4.0) 192 | ffi (~> 1.9) 193 | sassc-rails (2.1.2) 194 | railties (>= 4.0.0) 195 | sassc (>= 2.0) 196 | sprockets (> 3.0) 197 | sprockets-rails 198 | tilt 199 | selenium-webdriver (3.142.7) 200 | childprocess (>= 0.5, < 4.0) 201 | rubyzip (>= 1.2.2) 202 | semantic_range (3.0.0) 203 | sprockets (4.0.2) 204 | concurrent-ruby (~> 1.0) 205 | rack (> 1, < 3) 206 | sprockets-rails (3.2.2) 207 | actionpack (>= 4.0) 208 | activesupport (>= 4.0) 209 | sprockets (>= 3.0.0) 210 | sqlite3 (1.4.2) 211 | standard (1.1.5) 212 | rubocop (= 1.18.3) 213 | rubocop-performance (= 1.11.4) 214 | thor (1.1.0) 215 | tilt (2.0.10) 216 | turbolinks (5.2.1) 217 | turbolinks-source (~> 5.2) 218 | turbolinks-source (5.2.0) 219 | tzinfo (2.0.4) 220 | concurrent-ruby (~> 1.0) 221 | unicode-display_width (2.0.0) 222 | web-console (4.1.0) 223 | actionview (>= 6.0.0) 224 | activemodel (>= 6.0.0) 225 | bindex (>= 0.4.0) 226 | railties (>= 6.0.0) 227 | webdrivers (4.6.0) 228 | nokogiri (~> 1.6) 229 | rubyzip (>= 1.3.0) 230 | selenium-webdriver (>= 3.0, < 4.0) 231 | webpacker (5.4.0) 232 | activesupport (>= 5.2) 233 | rack-proxy (>= 0.6.1) 234 | railties (>= 5.2) 235 | semantic_range (>= 2.3.0) 236 | websocket-driver (0.7.5) 237 | websocket-extensions (>= 0.1.0) 238 | websocket-extensions (0.1.5) 239 | xpath (3.2.0) 240 | nokogiri (~> 1.8) 241 | zeitwerk (2.4.2) 242 | 243 | PLATFORMS 244 | ruby 245 | 246 | DEPENDENCIES 247 | bootsnap 248 | byebug 249 | capybara 250 | cypress-rails 251 | dotenv-rails 252 | jbuilder 253 | listen 254 | pry-rails 255 | puma 256 | rails 257 | sass-rails 258 | selenium-webdriver 259 | sqlite3 260 | standard 261 | static-rails! 262 | turbolinks 263 | tzinfo-data 264 | web-console 265 | webdrivers 266 | webpacker 267 | 268 | RUBY VERSION 269 | ruby 2.7.0p0 270 | 271 | BUNDLED WITH 272 | 2.2.15 273 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # static-rails-example 2 | 3 | This example uses the static-rails gem. To see how this is all configured, check 4 | out [config/initializers/static.rb](config/initializers/static.rb). 5 | 6 | ## Development mode 7 | 8 | See it in action in local development 9 | 10 | 1. Install Ruby & [hugo](https://gohugo.io/getting-started/installing/) 11 | 2. `cd static/docs && bundle` to set up the Jekyll app 12 | 3. `cd static/blog-docs && npm install` to set up the Eleventy app 13 | 4. Add this line to your system's `/etc/hosts` file: `127.0.0.1 blog.localhost` 14 | 5. In this project's root directory run `bundle && bin/rails s` 15 | 6. Visit any of the static sites within: 16 | * A Jekyll site at [http://localhost:3000/docs](localhost:3000/docs) 17 | * A Hugo site at [http://blog.localhost:3000](blog.localhost:3000) 18 | * An Eleventy site at [http://blog.localhost:3000/docs](blog.localhost:3000/docs) 19 | * A Hugo site at [http://localhost:3000](localhost:3000/marketing) 20 | 21 | ## Production mode 22 | 23 | See how things would look in production: 24 | 25 | 1. Run `bin/rake assets:precompile` 26 | 2. Inspect the built assets in 27 | * `static/docs/_site` 28 | * `static/blog/public` 29 | * `static/blog-docs/_site` 30 | * `static/marketing/public` 31 | 3. Run `RAILS_ENV=production bin/rails s` and verify the above URLs serve the 32 | production assets without running any dev servers 33 | 34 | ## Running tests 35 | 36 | This app also includes a Cypress test (via our 37 | [cypress-rails](https://github.com/testdouble/cypress-rails) gem). 38 | 39 | Run it with: 40 | 41 | ``` 42 | $ ./script/test 43 | ``` 44 | 45 | You can check out the [CI build 46 | output](https://circleci.com/gh/testdouble/static-rails), too. 47 | -------------------------------------------------------------------------------- /example/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /example/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | -------------------------------------------------------------------------------- /example/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/app/assets/images/.keep -------------------------------------------------------------------------------- /example/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /example/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /example/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /example/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /example/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /example/app/controllers/docs/api/houses_controller.rb: -------------------------------------------------------------------------------- 1 | module Docs 2 | module Api 3 | class HousesController < ApplicationController 4 | def create 5 | render json: { 6 | id: 22 7 | } 8 | end 9 | 10 | def show 11 | if params[:id] == "22" 12 | render json: { 13 | id: 22, 14 | fake: true 15 | } 16 | else 17 | head :internal_server_error 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /example/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /example/app/javascript/channels/consumer.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command. 3 | 4 | import { createConsumer } from "@rails/actioncable" 5 | 6 | export default createConsumer() 7 | -------------------------------------------------------------------------------- /example/app/javascript/channels/index.js: -------------------------------------------------------------------------------- 1 | // Load all the channels within this directory and all subdirectories. 2 | // Channel files must be named *_channel.js. 3 | 4 | const channels = require.context('.', true, /_channel\.js$/) 5 | channels.keys().forEach(channels) 6 | -------------------------------------------------------------------------------- /example/app/javascript/packs/application.js: -------------------------------------------------------------------------------- 1 | // This file is automatically compiled by Webpack, along with any other files 2 | // present in this directory. You're encouraged to place your actual application logic in 3 | // a relevant structure within app/javascript and only use these pack files to reference 4 | // that code so it'll be compiled. 5 | 6 | require("@rails/ujs").start() 7 | require("turbolinks").start() 8 | require("@rails/activestorage").start() 9 | require("channels") 10 | 11 | 12 | // Uncomment to copy all static images under ../images to the output folder and reference 13 | // them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>) 14 | // or the `imagePath` JavaScript helper below. 15 | // 16 | // const images = require.context('../images', true) 17 | // const imagePath = (name) => images(name, true) 18 | -------------------------------------------------------------------------------- /example/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /example/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "from@example.com" 3 | layout "mailer" 4 | end 5 | -------------------------------------------------------------------------------- /example/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /example/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/app/models/concerns/.keep -------------------------------------------------------------------------------- /example/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | <%= csrf_meta_tags %> 6 | <%= csp_meta_tag %> 7 | 8 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> 9 | <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> 10 | 11 | 12 | 13 | <%= yield %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | var validEnv = ['development', 'test', 'production'] 3 | var currentEnv = api.env() 4 | var isDevelopmentEnv = api.env('development') 5 | var isProductionEnv = api.env('production') 6 | var isTestEnv = api.env('test') 7 | 8 | if (!validEnv.includes(currentEnv)) { 9 | throw new Error( 10 | 'Please specify a valid `NODE_ENV` or ' + 11 | '`BABEL_ENV` environment variables. Valid values are "development", ' + 12 | '"test", and "production". Instead, received: ' + 13 | JSON.stringify(currentEnv) + 14 | '.' 15 | ) 16 | } 17 | 18 | return { 19 | presets: [ 20 | isTestEnv && [ 21 | '@babel/preset-env', 22 | { 23 | targets: { 24 | node: 'current' 25 | } 26 | } 27 | ], 28 | (isProductionEnv || isDevelopmentEnv) && [ 29 | '@babel/preset-env', 30 | { 31 | forceAllTransforms: true, 32 | useBuiltIns: 'entry', 33 | corejs: 3, 34 | modules: false, 35 | exclude: ['transform-typeof-symbol'] 36 | } 37 | ] 38 | ].filter(Boolean), 39 | plugins: [ 40 | 'babel-plugin-macros', 41 | '@babel/plugin-syntax-dynamic-import', 42 | isTestEnv && 'babel-plugin-dynamic-import-node', 43 | '@babel/plugin-transform-destructuring', 44 | [ 45 | '@babel/plugin-proposal-class-properties', 46 | { 47 | loose: true 48 | } 49 | ], 50 | [ 51 | '@babel/plugin-proposal-object-rest-spread', 52 | { 53 | useBuiltIns: true 54 | } 55 | ], 56 | [ 57 | '@babel/plugin-transform-runtime', 58 | { 59 | helpers: false, 60 | regenerator: true, 61 | corejs: false 62 | } 63 | ], 64 | [ 65 | '@babel/plugin-transform-regenerator', 66 | { 67 | async: false 68 | } 69 | ] 70 | ].filter(Boolean) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /example/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_version 64 | @bundler_version ||= 65 | env_var_version || cli_arg_version || 66 | lockfile_version 67 | end 68 | 69 | def bundler_requirement 70 | return "#{Gem::Requirement.default}.a" unless bundler_version 71 | 72 | bundler_gem_version = Gem::Version.new(bundler_version) 73 | 74 | requirement = bundler_gem_version.approximate_recommendation 75 | 76 | return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") 77 | 78 | requirement += ".a" if bundler_gem_version.prerelease? 79 | 80 | requirement 81 | end 82 | 83 | def load_bundler! 84 | ENV["BUNDLE_GEMFILE"] ||= gemfile 85 | 86 | activate_bundler 87 | end 88 | 89 | def activate_bundler 90 | gem_error = activation_error_handling do 91 | gem "bundler", bundler_requirement 92 | end 93 | return if gem_error.nil? 94 | require_error = activation_error_handling do 95 | require "bundler/version" 96 | end 97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 98 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 99 | exit 42 100 | end 101 | 102 | def activation_error_handling 103 | yield 104 | nil 105 | rescue StandardError, LoadError => e 106 | e 107 | end 108 | end 109 | 110 | m.load_bundler! 111 | 112 | if m.invoked_as_script? 113 | load Gem.bin_path("bundler", "bundle") 114 | end 115 | -------------------------------------------------------------------------------- /example/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /example/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /example/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path('..', __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to setup or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies 21 | # system('bin/yarn') 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! 'bin/rails db:prepare' 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! 'bin/rails log:clear tmp:clear' 33 | 34 | puts "\n== Restarting application server ==" 35 | system! 'bin/rails restart' 36 | end 37 | -------------------------------------------------------------------------------- /example/bin/webpack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/webpack_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::WebpackRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /example/bin/webpack-dev-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/dev_server_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::DevServerRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /example/bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | begin 5 | exec "yarnpkg", *ARGV 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /example/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /example/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Example 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 6.0 13 | 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration can go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded after loading 17 | # the framework and any gems in your application. 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /example/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /example/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: example_production 11 | -------------------------------------------------------------------------------- /example/config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | zBam0OzM39Zga5qVnBRga0hpA5erOO7mA9pUBOoXbk1Ur7YTMlgJfS9K59tyQBzHJB49MbGCLTzJOq23TzHH12gONTWPFyiOgK005Vci+B3rZzefxm/3D3u4YdvAfc/if2k+a97XdKBj0Z+fP11BBiuhlP3Ca398p4YL4Jt433fjXQQWoCRdI/ntN4B6dxJrno1i1C3fKZi4OMzfdP5xU1iS+vcvw8u484DNdb90cPVXLaIt8elhnRIOIMMtepWuOzRGSNn2FAQGTs9IDL4+0xj6AlSYvegT/zmtRMroNsqZxQ+K6qY85uPVYVswt4HnpI1nWBVtDyHg2uaTjcjtPEFKUQMp6dA3ABkMW9Gz5AyKyPB6WKX86NZnOG7RsCCO1hnzv2DZo5QP5WAR2hKagz3oVWPQjOm9tWls--c7mnZ8UROCfsFNdq--yt07Y7Ud//X0znGllixJ6A== -------------------------------------------------------------------------------- /example/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /example/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /example/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | # Run rails dev:cache to toggle caching. 17 | if Rails.root.join("tmp", "caching-dev.txt").exist? 18 | config.action_controller.perform_caching = true 19 | config.action_controller.enable_fragment_cache_logging = true 20 | 21 | config.cache_store = :memory_store 22 | config.public_file_server.headers = { 23 | "Cache-Control" => "public, max-age=#{2.days.to_i}" 24 | } 25 | else 26 | config.action_controller.perform_caching = false 27 | 28 | config.cache_store = :null_store 29 | end 30 | 31 | # Store uploaded files on the local file system (see config/storage.yml for options). 32 | config.active_storage.service = :local 33 | 34 | # Don't care if the mailer can't send. 35 | config.action_mailer.raise_delivery_errors = false 36 | 37 | config.action_mailer.perform_caching = false 38 | 39 | # Print deprecation notices to the Rails logger. 40 | config.active_support.deprecation = :log 41 | 42 | # Raise an error on page load if there are pending migrations. 43 | config.active_record.migration_error = :page_load 44 | 45 | # Highlight code that triggered database queries in logs. 46 | config.active_record.verbose_query_logs = true 47 | 48 | # Debug mode disables concatenation and preprocessing of assets. 49 | # This option may cause significant delays in view rendering with a large 50 | # number of complex assets. 51 | config.assets.debug = true 52 | 53 | # Suppress logger output for asset requests. 54 | config.assets.quiet = true 55 | 56 | # Raises error for missing translations. 57 | # config.action_view.raise_on_missing_translations = true 58 | 59 | # Use an evented file watcher to asynchronously detect changes in source code, 60 | # routes, locales, etc. This feature depends on the listen gem. 61 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 62 | end 63 | -------------------------------------------------------------------------------- /example/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = true 15 | config.action_controller.perform_caching = true 16 | 17 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 18 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 19 | # config.require_master_key = true 20 | 21 | # Disable serving static files from the `/public` folder by default since 22 | # Apache or NGINX already handles this. 23 | config.public_file_server.enabled = true # ENV["RAILS_SERVE_STATIC_FILES"].present? 24 | 25 | # Compress CSS using a preprocessor. 26 | # config.assets.css_compressor = :sass 27 | 28 | # Do not fallback to assets pipeline if a precompiled asset is missed. 29 | config.assets.compile = false 30 | 31 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 32 | # config.action_controller.asset_host = 'http://assets.example.com' 33 | 34 | # Specifies the header that your server uses for sending files. 35 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 36 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 37 | 38 | # Store uploaded files on the local file system (see config/storage.yml for options). 39 | config.active_storage.service = :local 40 | 41 | # Mount Action Cable outside main process or domain. 42 | # config.action_cable.mount_path = nil 43 | # config.action_cable.url = 'wss://example.com/cable' 44 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 45 | 46 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 47 | # config.force_ssl = true 48 | 49 | # Use the lowest log level to ensure availability of diagnostic information 50 | # when problems arise. 51 | config.log_level = :debug 52 | 53 | # Prepend all log lines with the following tags. 54 | config.log_tags = [:request_id] 55 | 56 | # Use a different cache store in production. 57 | # config.cache_store = :mem_cache_store 58 | 59 | # Use a real queuing backend for Active Job (and separate queues per environment). 60 | # config.active_job.queue_adapter = :resque 61 | # config.active_job.queue_name_prefix = "example_production" 62 | 63 | config.action_mailer.perform_caching = false 64 | 65 | # Ignore bad email addresses and do not raise email delivery errors. 66 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 67 | # config.action_mailer.raise_delivery_errors = false 68 | 69 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 70 | # the I18n.default_locale when a translation cannot be found). 71 | config.i18n.fallbacks = true 72 | 73 | # Send deprecation notices to registered listeners. 74 | config.active_support.deprecation = :notify 75 | 76 | # Use default logging formatter so that PID and timestamp are not suppressed. 77 | config.log_formatter = ::Logger::Formatter.new 78 | 79 | # Use a different logger for distributed setups. 80 | # require 'syslog/logger' 81 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 82 | 83 | if ENV["RAILS_LOG_TO_STDOUT"].present? 84 | logger = ActiveSupport::Logger.new(STDOUT) 85 | logger.formatter = config.log_formatter 86 | config.logger = ActiveSupport::TaggedLogging.new(logger) 87 | end 88 | 89 | # Do not dump schema after migrations. 90 | config.active_record.dump_schema_after_migration = false 91 | 92 | # Inserts middleware to perform automatic connection switching. 93 | # The `database_selector` hash is used to pass options to the DatabaseSelector 94 | # middleware. The `delay` is used to determine how long to wait after a write 95 | # to send a subsequent read to the primary. 96 | # 97 | # The `database_resolver` class is used by the middleware to determine which 98 | # database is appropriate to use based on the time delay. 99 | # 100 | # The `database_resolver_context` class is used by the middleware to set 101 | # timestamps for the last write to the primary. The resolver uses the context 102 | # class timestamps to determine how long to wait before reading from the 103 | # replica. 104 | # 105 | # By default Rails will store a last write timestamp in the session. The 106 | # DatabaseSelector middleware is designed as such you can define your own 107 | # strategy for connection switching and pass that into the middleware through 108 | # these configuration options. 109 | # config.active_record.database_selector = { delay: 2.seconds } 110 | # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver 111 | # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session 112 | end 113 | -------------------------------------------------------------------------------- /example/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # The test environment is used exclusively to run your application's 2 | # test suite. You never need to work with it otherwise. Remember that 3 | # your test database is "scratch space" for the test suite and is wiped 4 | # and recreated between test runs. Don't rely on the data there! 5 | 6 | Rails.application.configure do 7 | # Settings specified here will take precedence over those in config/application.rb. 8 | 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. This avoids loading your whole application 12 | # just for the purpose of running a single test. If you are using a tool that 13 | # preloads Rails for running tests, you may have to set it to true. 14 | config.eager_load = false 15 | 16 | # Configure public file server for tests with Cache-Control for performance. 17 | config.public_file_server.enabled = true 18 | config.public_file_server.headers = { 19 | "Cache-Control" => "public, max-age=3600" 20 | } 21 | 22 | # Show full error reports and disable caching. 23 | config.consider_all_requests_local = true 24 | config.action_controller.perform_caching = false 25 | config.cache_store = :null_store 26 | 27 | # Raise exceptions instead of rendering exception templates. 28 | config.action_dispatch.show_exceptions = false 29 | 30 | # Enable request forgery protection in test environment - we have a feature that depends on this working! 31 | config.action_controller.allow_forgery_protection = true 32 | 33 | # Store uploaded files on the local file system in a temporary directory. 34 | config.active_storage.service = :test 35 | 36 | config.action_mailer.perform_caching = false 37 | 38 | # Tell Action Mailer not to deliver emails to the real world. 39 | # The :test delivery method accumulates sent emails in the 40 | # ActionMailer::Base.deliveries array. 41 | config.action_mailer.delivery_method = :test 42 | 43 | # Print deprecation notices to the stderr. 44 | config.active_support.deprecation = :stderr 45 | 46 | # Raises error for missing translations. 47 | # config.action_view.raise_on_missing_translations = true 48 | end 49 | -------------------------------------------------------------------------------- /example/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /example/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join("node_modules") 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /example/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /example/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | # # If you are using webpack-dev-server then specify webpack-dev-server host 15 | # policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? 16 | 17 | # # Specify URI for violation reports 18 | # # policy.report_uri "/csp-violation-report-endpoint" 19 | # end 20 | 21 | # If you are using UJS then enable automatic nonce generation 22 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 23 | 24 | # Set the nonce only to specific directives 25 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 26 | 27 | # Report CSP violations to a specified URI 28 | # For further information see the following documentation: 29 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 30 | # Rails.application.config.content_security_policy_report_only = true 31 | -------------------------------------------------------------------------------- /example/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /example/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /example/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /example/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /example/config/initializers/static.rb: -------------------------------------------------------------------------------- 1 | StaticRails.config do |config| 2 | config.set_csrf_token_cookie = true 3 | 4 | config.sites = [ 5 | { 6 | name: "blog-docs", 7 | url_subdomain: "blog", 8 | url_root_path: "/docs", 9 | url_skip_paths_starting_with: ["/docs/api/"], 10 | source_dir: "static/blog-docs", 11 | server_command: "npx @11ty/eleventy --serve --pathprefix /docs", 12 | server_port: 8080, 13 | compile_command: "npx @11ty/eleventy --pathprefix /docs", 14 | compile_dir: "static/blog-docs/_site" 15 | }, 16 | { 17 | name: "blog", 18 | url_subdomain: "blog", 19 | url_root_path: "/", 20 | url_skip_paths_starting_with: ["/docs/api/"], 21 | source_dir: "static/blog", 22 | server_command: "hugo server --disableLiveReload", 23 | server_port: 1313, 24 | compile_command: "hugo", 25 | compile_dir: "static/blog/public" 26 | }, 27 | { 28 | name: "docs", 29 | url_root_path: "/docs", 30 | url_skip_paths_starting_with: ["/docs/api/"], 31 | source_dir: "static/docs", 32 | env: {"BUNDLE_PATH" => "vendor/bundle"}, 33 | server_command: "bundle exec jekyll serve", 34 | server_port: 4000, 35 | compile_command: "bundle exec jekyll build", 36 | compile_dir: "static/docs/_site" 37 | }, 38 | { 39 | name: "marketing", 40 | url_root_path: "/marketing", 41 | source_dir: "static/marketing", 42 | server_command: "hugo server -p 1314 --disableLiveReload", 43 | server_port: 1314, 44 | compile_command: "hugo", 45 | compile_dir: "static/marketing/public", 46 | compile_404_file_path: "404.html" 47 | } 48 | ] 49 | end 50 | -------------------------------------------------------------------------------- /example/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /example/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /example/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 12 | # 13 | port ENV.fetch("PORT") { 3000 } 14 | 15 | # Specifies the `environment` that Puma will run in. 16 | # 17 | environment ENV.fetch("RAILS_ENV") { "development" } 18 | 19 | # Specifies the `pidfile` that Puma will use. 20 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 21 | 22 | # Specifies the number of `workers` to boot in clustered mode. 23 | # Workers are forked web server processes. If using threads and workers together 24 | # the concurrency of the application would be max `threads` * `workers`. 25 | # Workers do not work on JRuby or Windows (both of which do not support 26 | # processes). 27 | # 28 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 29 | 30 | # Use the `preload_app!` method when specifying a `workers` number. 31 | # This directive tells Puma to first boot the application and load code 32 | # before forking the application. This takes advantage of Copy On Write 33 | # process behavior so workers use less memory. 34 | # 35 | # preload_app! 36 | 37 | # Allow puma to be restarted by `rails restart` command. 38 | plugin :tmp_restart 39 | -------------------------------------------------------------------------------- /example/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | constraints ->(req) { req.host.start_with?("blog") } do 3 | namespace :docs do 4 | namespace :api do 5 | resources :houses 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /example/config/spring.rb: -------------------------------------------------------------------------------- 1 | Spring.watch( 2 | ".ruby-version", 3 | ".rbenv-vars", 4 | "tmp/restart.txt", 5 | "tmp/caching-dev.txt" 6 | ) 7 | -------------------------------------------------------------------------------- /example/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /example/config/webpack/development.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /example/config/webpack/environment.js: -------------------------------------------------------------------------------- 1 | const { environment } = require('@rails/webpacker') 2 | 3 | module.exports = environment 4 | -------------------------------------------------------------------------------- /example/config/webpack/production.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /example/config/webpack/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /example/config/webpacker.yml: -------------------------------------------------------------------------------- 1 | # Note: You must restart bin/webpack-dev-server for changes to take effect 2 | 3 | default: &default 4 | source_path: app/javascript 5 | source_entry_path: packs 6 | public_root_path: public 7 | public_output_path: packs 8 | cache_path: tmp/cache/webpacker 9 | check_yarn_integrity: false 10 | webpack_compile_output: true 11 | 12 | # Additional paths webpack should lookup modules 13 | # ['app/assets', 'engine/foo/app/assets'] 14 | resolved_paths: [] 15 | 16 | # Reload manifest.json on all requests so we reload latest compiled packs 17 | cache_manifest: false 18 | 19 | # Extract and emit a css file 20 | extract_css: false 21 | 22 | static_assets_extensions: 23 | - .jpg 24 | - .jpeg 25 | - .png 26 | - .gif 27 | - .tiff 28 | - .ico 29 | - .svg 30 | - .eot 31 | - .otf 32 | - .ttf 33 | - .woff 34 | - .woff2 35 | 36 | extensions: 37 | - .mjs 38 | - .js 39 | - .sass 40 | - .scss 41 | - .css 42 | - .module.sass 43 | - .module.scss 44 | - .module.css 45 | - .png 46 | - .svg 47 | - .gif 48 | - .jpeg 49 | - .jpg 50 | 51 | development: 52 | <<: *default 53 | compile: true 54 | 55 | # Verifies that correct packages and versions are installed by inspecting package.json, yarn.lock, and node_modules 56 | check_yarn_integrity: true 57 | 58 | # Reference: https://webpack.js.org/configuration/dev-server/ 59 | dev_server: 60 | https: false 61 | host: localhost 62 | port: 3035 63 | public: localhost:3035 64 | hmr: false 65 | # Inline should be set to true if using HMR 66 | inline: true 67 | overlay: true 68 | compress: true 69 | disable_host_check: true 70 | use_local_ip: false 71 | quiet: false 72 | pretty: false 73 | headers: 74 | 'Access-Control-Allow-Origin': '*' 75 | watch_options: 76 | ignored: '**/node_modules/**' 77 | 78 | 79 | test: 80 | <<: *default 81 | compile: true 82 | 83 | # Compile test packs to a separate directory 84 | public_output_path: packs-test 85 | 86 | production: 87 | <<: *default 88 | 89 | # Production depends on precompilation of packs prior to booting for performance. 90 | compile: false 91 | 92 | # Extract and emit a css file 93 | extract_css: true 94 | 95 | # Cache manifest.json for performance 96 | cache_manifest: true 97 | -------------------------------------------------------------------------------- /example/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "screenshotsFolder": "tmp/cypress_screenshots", 3 | "videosFolder": "tmp/cypress_videos", 4 | "trashAssetsBeforeRuns": false 5 | } 6 | -------------------------------------------------------------------------------- /example/cypress/integration/static-rails.js: -------------------------------------------------------------------------------- 1 | 2 | describe('rails-static stuff seems to work', () => { 3 | it('accesses the 11ty site at blog.localhost/docs', () => { 4 | // Make sure the literal index HTML file works 5 | cy.visit(`http://blog.localhost:3009/docs/index.html`) 6 | cy.get('p').should('have.text', 'Hi') 7 | 8 | // Also make sure hitting the directory without trailing / works 9 | cy.visit(`http://blog.localhost:3009/docs`) 10 | cy.get('p').should('have.text', 'Hi') 11 | cy.get('#api-result').should('contain.text', 'API liked our CSRF token and sent back ID: 22') 12 | }) 13 | 14 | it('accesses the hugo site at blog.localhost', () => { 15 | cy.visit('http://blog.localhost:3009') 16 | 17 | cy.get('header a') 18 | .should('contain.text', 'My Blerg') 19 | .should('have.attr', 'href', '/') 20 | 21 | cy.contains('My First Post').click() 22 | cy.get('article').should('contain.text', 'I am a post') 23 | }) 24 | 25 | it('accesses the jekyll site at /docs', () => { 26 | cy.visit('http://localhost:3009/docs') 27 | 28 | cy.get('.post-link').click() 29 | cy.get('.post-content').should('contain.text', 'find this post in your') 30 | }) 31 | 32 | it('accesses the hugo site at /marketing', () => { 33 | cy.visit('http://localhost:3009/marketing') 34 | 35 | cy.get('header a') 36 | .should('contain.text', 'My Marketing Page') 37 | .should('have.attr', 'href', '/marketing/') 38 | 39 | cy.contains('My First Post').click() 40 | cy.get('article').should('contain.text', 'I am a post') 41 | }) 42 | 43 | it('can access an API carved out within where the 11ty site is mounted', () => { 44 | cy.visit('http://blog.localhost:3009/docs') 45 | cy.getCookie('_csrf_token').then((cookie) => { 46 | cy.request({ 47 | url: 'http://blog.localhost:3009/docs/api/houses', 48 | method: 'POST', 49 | headers: { 50 | 'x-csrf-token': decodeURIComponent(cookie.value) 51 | } 52 | }).then((res) => { 53 | cy.request(`http://blog.localhost:3009/docs/api/houses/${res.body.id}`).should((response) => { 54 | expect(response.status).to.eq(200) 55 | expect(response.body).to.have.property('id', 22) 56 | expect(response.body).to.have.property('fake', true) 57 | }) 58 | }) 59 | }) 60 | }) 61 | 62 | it('[Production only] assign better cache-control for images', () => { 63 | if (Cypress.env('RAILS_ENV') !== 'production') return 64 | 65 | cy.request('http://localhost:3009/docs/assets/an_image.png').should((response) => { 66 | expect(response.headers).to.include({ 67 | 'cache-control': 'public; max-age=31536000', 68 | 'content-length': '4320', 69 | 'content-type': 'image/png' 70 | }) 71 | 72 | const lastModified = response.headers['last-modified'] 73 | expect(lastModified).not.to.be.empty 74 | 75 | const preModified = new Date(new Date(lastModified).getTime() - 10000).toUTCString() 76 | cy.request({ 77 | url: 'http://localhost:3009/docs/assets/an_image.png', 78 | headers: { 79 | 'if-modified-since': preModified 80 | } 81 | }).should((response) => { 82 | expect(response.status).to.eq(200) 83 | }) 84 | 85 | const postModified = new Date(new Date(lastModified).getTime() + 10000).toUTCString() 86 | cy.request({ 87 | url: 'http://localhost:3009/docs/assets/an_image.png', 88 | headers: { 89 | 'if-modified-since': postModified 90 | } 91 | }).should((response) => { 92 | expect(response.status).to.eq(304) 93 | }) 94 | }) 95 | }) 96 | 97 | it('[Production only] cache-control no cache for HTML', () => { 98 | if (Cypress.env('RAILS_ENV') !== 'production') return 99 | 100 | cy.request('http://blog.localhost:3009/docs/index.html').should((response) => { 101 | expect(response.headers).to.include({ 102 | 'cache-control': 'no-cache, no-store', 103 | // 'content-length': '4320', 104 | 'content-type': 'text/html' 105 | }) 106 | }) 107 | }) 108 | 109 | it('[Production only] gzips most file types', () => { 110 | if (Cypress.env('RAILS_ENV') !== 'production') return 111 | 112 | cy.request({ 113 | url: 'http://localhost:3009/docs/assets/a_javascript.js', 114 | headers: { 115 | 'accept-encoding': 'gzip' 116 | } 117 | }).should(response => { 118 | expect(response.headers).to.include({ 119 | 'vary': 'Accept-Encoding', 120 | 'content-encoding': 'gzip', 121 | 'content-length': '58', 122 | 'content-type': 'application/javascript' 123 | }) 124 | }) 125 | 126 | cy.request({ 127 | url: 'http://localhost:3009/docs/assets/a_json.json', 128 | headers: { 129 | 'accept-encoding': 'gzip' 130 | } 131 | }).should(response => { 132 | expect(response.headers).to.include({ 133 | 'vary': 'Accept-Encoding', 134 | 'content-encoding': 'gzip', 135 | 'content-length': '58', 136 | 'content-type': 'application/json' 137 | }) 138 | }) 139 | 140 | cy.request({ 141 | url: 'http://localhost:3009/docs/assets/a_css.css', 142 | headers: { 143 | 'accept-encoding': 'gzip' 144 | } 145 | }).should(response => { 146 | expect(response.headers).to.include({ 147 | 'vary': 'Accept-Encoding', 148 | 'content-encoding': 'gzip', 149 | 'content-type': 'text/css' 150 | }) 151 | }) 152 | 153 | cy.request({ 154 | url: 'http://localhost:3009/docs/assets/a_html.html', 155 | headers: { 156 | 'accept-encoding': 'gzip' 157 | } 158 | }).should(response => { 159 | expect(response.headers).to.include({ 160 | 'vary': 'Accept-Encoding', 161 | 'content-encoding': 'gzip', 162 | 'content-type': 'text/html' 163 | }) 164 | }) 165 | 166 | cy.request({ 167 | url: 'http://localhost:3009/docs/assets/an_image.png', 168 | headers: { 169 | 'accept-encoding': 'gzip' 170 | } 171 | }).should(response => { 172 | expect(response.headers['content-encoding']).to.be.undefined 173 | expect(response.headers['vary']).to.be.undefined 174 | expect(response.headers).to.include({ 175 | 'content-type': 'image/png' 176 | }) 177 | }) 178 | }) 179 | 180 | it('[Production only] will serve up 404 pages with 404 status code', () => { 181 | if (Cypress.env('RAILS_ENV') !== 'production') return 182 | cy.visit('http://localhost:3009/marketing/fooberry', { 183 | failOnStatusCode: false 184 | }) 185 | 186 | cy.contains('I am a 404 page for /marketing') 187 | 188 | cy.request({ 189 | url: 'http://localhost:3009/marketing/lmao', 190 | failOnStatusCode: false 191 | }).should(res => { 192 | expect(res.status).to.eq(404) 193 | }) 194 | }) 195 | 196 | 197 | }) 198 | -------------------------------------------------------------------------------- /example/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | } 22 | -------------------------------------------------------------------------------- /example/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /example/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | // Force the _csrf_token to persist across tests, which will exercise our 23 | // SitePlusCsrfMiddleware middleware's fixing of invalid tokens 24 | Cypress.Cookies.defaults({ 25 | preserve: "_csrf_token" 26 | }) 27 | -------------------------------------------------------------------------------- /example/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /example/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/lib/assets/.keep -------------------------------------------------------------------------------- /example/lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/lib/tasks/.keep -------------------------------------------------------------------------------- /example/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/log/.keep -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "private": true, 4 | "dependencies": { 5 | "@rails/actioncable": "^6.0.0", 6 | "@rails/activestorage": "^6.0.0", 7 | "@rails/ujs": "^6.0.0", 8 | "@rails/webpacker": "^5.4.0", 9 | "turbolinks": "^5.2.0" 10 | }, 11 | "version": "0.1.0", 12 | "devDependencies": { 13 | "cypress": "^8.0.0", 14 | "webpack-dev-server": "^3.10.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('postcss-flexbugs-fixes'), 5 | require('postcss-preset-env')({ 6 | autoprefixer: { 7 | flexbox: 'no-2009' 8 | }, 9 | stage: 3 10 | }) 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /example/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /example/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /example/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /example/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /example/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/public/apple-touch-icon.png -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | Hello I am an Internet 2 | -------------------------------------------------------------------------------- /example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /example/script/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | echo "--> Running tests in development, starting all static servers and proxying requests" 6 | bin/rake cypress:run 7 | 8 | echo "--> Precompiling all static site assets with rake assets:precompile" 9 | bin/rake assets:precompile 10 | 11 | echo "--> Running tests in production, *not* starting servers and serving files instead of proxying" 12 | RAILS_ENV=production CYPRESS_RAILS_CYPRESS_OPTS="--env RAILS_ENV=production" bin/rake cypress:run 13 | -------------------------------------------------------------------------------- /example/static/blog-docs/.gitignore: -------------------------------------------------------------------------------- 1 | /_site 2 | /node_modules 3 | -------------------------------------------------------------------------------- /example/static/blog-docs/README.md: -------------------------------------------------------------------------------- 1 | # Page header 2 | -------------------------------------------------------------------------------- /example/static/blog-docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page title 5 | 6 | 7 |

Hi

8 |
9 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /example/static/blog-docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog-docs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "@11ty/eleventy": "^0.12.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/static/blog/.gitignore: -------------------------------------------------------------------------------- 1 | /resources 2 | /public 3 | -------------------------------------------------------------------------------- /example/static/blog/archetypes/default.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "{{ replace .Name "-" " " | title }}" 3 | date: {{ .Date }} 4 | draft: true 5 | --- 6 | 7 | -------------------------------------------------------------------------------- /example/static/blog/config.toml: -------------------------------------------------------------------------------- 1 | baseURL = "http://blog.example.com/" 2 | languageCode = "en-us" 3 | title = "My Blerg" 4 | theme = "ultralight" 5 | -------------------------------------------------------------------------------- /example/static/blog/content/posts/my-first-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "My First Post" 3 | date: 2020-04-29T16:49:04+09:00 4 | --- 5 | h2 Hello 6 | 7 | I am a post 8 | 9 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/archetypes/default.md: -------------------------------------------------------------------------------- 1 | +++ 2 | +++ 3 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/assets/js/index.js: -------------------------------------------------------------------------------- 1 | // JS can go here. 2 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/assets/sass/code.css: -------------------------------------------------------------------------------- 1 | /* LIGHT MODE */ 2 | /* hugo gen chromastyles --style autumn */ 3 | 4 | /* Background */ .chroma { background-color: #ffffff } 5 | /* Error */ .chroma .err { color: #ff0000; background-color: #ffaaaa } 6 | /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } 7 | /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; } 8 | /* LineHighlight */ .chroma .hl { display: block; width: 100%;background-color: #ffffcc } 9 | /* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } 10 | /* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } 11 | /* Keyword */ .chroma .k { color: #0000aa } 12 | /* KeywordConstant */ .chroma .kc { color: #0000aa } 13 | /* KeywordDeclaration */ .chroma .kd { color: #0000aa } 14 | /* KeywordNamespace */ .chroma .kn { color: #0000aa } 15 | /* KeywordPseudo */ .chroma .kp { color: #0000aa } 16 | /* KeywordReserved */ .chroma .kr { color: #0000aa } 17 | /* KeywordType */ .chroma .kt { color: #00aaaa } 18 | /* NameAttribute */ .chroma .na { color: #1e90ff } 19 | /* NameBuiltin */ .chroma .nb { color: #00aaaa } 20 | /* NameClass */ .chroma .nc { color: #00aa00; text-decoration: underline } 21 | /* NameConstant */ .chroma .no { color: #aa0000 } 22 | /* NameDecorator */ .chroma .nd { color: #888888 } 23 | /* NameEntity */ .chroma .ni { color: #880000; font-weight: bold } 24 | /* NameFunction */ .chroma .nf { color: #00aa00 } 25 | /* NameNamespace */ .chroma .nn { color: #00aaaa; text-decoration: underline } 26 | /* NameTag */ .chroma .nt { color: #1e90ff; font-weight: bold } 27 | /* NameVariable */ .chroma .nv { color: #aa0000 } 28 | /* LiteralString */ .chroma .s { color: #aa5500 } 29 | /* LiteralStringAffix */ .chroma .sa { color: #aa5500 } 30 | /* LiteralStringBacktick */ .chroma .sb { color: #aa5500 } 31 | /* LiteralStringChar */ .chroma .sc { color: #aa5500 } 32 | /* LiteralStringDelimiter */ .chroma .dl { color: #aa5500 } 33 | /* LiteralStringDoc */ .chroma .sd { color: #aa5500 } 34 | /* LiteralStringDouble */ .chroma .s2 { color: #aa5500 } 35 | /* LiteralStringEscape */ .chroma .se { color: #aa5500 } 36 | /* LiteralStringHeredoc */ .chroma .sh { color: #aa5500 } 37 | /* LiteralStringInterpol */ .chroma .si { color: #aa5500 } 38 | /* LiteralStringOther */ .chroma .sx { color: #aa5500 } 39 | /* LiteralStringRegex */ .chroma .sr { color: #009999 } 40 | /* LiteralStringSingle */ .chroma .s1 { color: #aa5500 } 41 | /* LiteralStringSymbol */ .chroma .ss { color: #0000aa } 42 | /* LiteralNumber */ .chroma .m { color: #009999 } 43 | /* LiteralNumberBin */ .chroma .mb { color: #009999 } 44 | /* LiteralNumberFloat */ .chroma .mf { color: #009999 } 45 | /* LiteralNumberHex */ .chroma .mh { color: #009999 } 46 | /* LiteralNumberInteger */ .chroma .mi { color: #009999 } 47 | /* LiteralNumberIntegerLong */ .chroma .il { color: #009999 } 48 | /* LiteralNumberOct */ .chroma .mo { color: #009999 } 49 | /* OperatorWord */ .chroma .ow { color: #0000aa } 50 | /* Comment */ .chroma .c { color: #aaaaaa; font-style: italic } 51 | /* CommentHashbang */ .chroma .ch { color: #aaaaaa; font-style: italic } 52 | /* CommentMultiline */ .chroma .cm { color: #aaaaaa; font-style: italic } 53 | /* CommentSingle */ .chroma .c1 { color: #aaaaaa; font-style: italic } 54 | /* CommentSpecial */ .chroma .cs { color: #0000aa; font-style: italic } 55 | /* CommentPreproc */ .chroma .cp { color: #4c8317 } 56 | /* CommentPreprocFile */ .chroma .cpf { color: #4c8317 } 57 | /* GenericDeleted */ .chroma .gd { color: #aa0000 } 58 | /* GenericEmph */ .chroma .ge { font-style: italic } 59 | /* GenericError */ .chroma .gr { color: #aa0000 } 60 | /* GenericHeading */ .chroma .gh { color: #000080; font-weight: bold } 61 | /* GenericInserted */ .chroma .gi { color: #00aa00 } 62 | /* GenericOutput */ .chroma .go { color: #888888 } 63 | /* GenericPrompt */ .chroma .gp { color: #555555 } 64 | /* GenericStrong */ .chroma .gs { font-weight: bold } 65 | /* GenericSubheading */ .chroma .gu { color: #800080; font-weight: bold } 66 | /* GenericTraceback */ .chroma .gt { color: #aa0000 } 67 | /* GenericUnderline */ .chroma .gl { text-decoration: underline } 68 | /* TextWhitespace */ .chroma .w { color: #bbbbbb } 69 | 70 | /* LIGHT MODE OVERRIDES: */ 71 | /* Background */ .chroma { background-color: #f6f6f6 } 72 | 73 | 74 | /* DARK MODE */ 75 | @media (prefers-color-scheme: dark) { 76 | /* hugo gen chromastyles --style monokai */ 77 | /* Background */ .chroma { color: #f8f8f2; background-color: #272822 } 78 | /* Error */ .chroma .err { color: #960050; background-color: #1e0010 } 79 | /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } 80 | /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; } 81 | /* LineHighlight */ .chroma .hl { display: block; width: 100%;background-color: #ffffcc } 82 | /* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } 83 | /* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } 84 | /* Keyword */ .chroma .k { color: #66d9ef } 85 | /* KeywordConstant */ .chroma .kc { color: #66d9ef } 86 | /* KeywordDeclaration */ .chroma .kd { color: #66d9ef } 87 | /* KeywordNamespace */ .chroma .kn { color: #f92672 } 88 | /* KeywordPseudo */ .chroma .kp { color: #66d9ef } 89 | /* KeywordReserved */ .chroma .kr { color: #66d9ef } 90 | /* KeywordType */ .chroma .kt { color: #66d9ef } 91 | /* NameAttribute */ .chroma .na { color: #a6e22e } 92 | /* NameClass */ .chroma .nc { color: #a6e22e } 93 | /* NameConstant */ .chroma .no { color: #66d9ef } 94 | /* NameDecorator */ .chroma .nd { color: #a6e22e } 95 | /* NameException */ .chroma .ne { color: #a6e22e } 96 | /* NameFunction */ .chroma .nf { color: #a6e22e } 97 | /* NameOther */ .chroma .nx { color: #a6e22e } 98 | /* NameTag */ .chroma .nt { color: #f92672 } 99 | /* Literal */ .chroma .l { color: #ae81ff } 100 | /* LiteralDate */ .chroma .ld { color: #e6db74 } 101 | /* LiteralString */ .chroma .s { color: #e6db74 } 102 | /* LiteralStringAffix */ .chroma .sa { color: #e6db74 } 103 | /* LiteralStringBacktick */ .chroma .sb { color: #e6db74 } 104 | /* LiteralStringChar */ .chroma .sc { color: #e6db74 } 105 | /* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 } 106 | /* LiteralStringDoc */ .chroma .sd { color: #e6db74 } 107 | /* LiteralStringDouble */ .chroma .s2 { color: #e6db74 } 108 | /* LiteralStringEscape */ .chroma .se { color: #ae81ff } 109 | /* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 } 110 | /* LiteralStringInterpol */ .chroma .si { color: #e6db74 } 111 | /* LiteralStringOther */ .chroma .sx { color: #e6db74 } 112 | /* LiteralStringRegex */ .chroma .sr { color: #e6db74 } 113 | /* LiteralStringSingle */ .chroma .s1 { color: #e6db74 } 114 | /* LiteralStringSymbol */ .chroma .ss { color: #e6db74 } 115 | /* LiteralNumber */ .chroma .m { color: #ae81ff } 116 | /* LiteralNumberBin */ .chroma .mb { color: #ae81ff } 117 | /* LiteralNumberFloat */ .chroma .mf { color: #ae81ff } 118 | /* LiteralNumberHex */ .chroma .mh { color: #ae81ff } 119 | /* LiteralNumberInteger */ .chroma .mi { color: #ae81ff } 120 | /* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff } 121 | /* LiteralNumberOct */ .chroma .mo { color: #ae81ff } 122 | /* Operator */ .chroma .o { color: #f92672 } 123 | /* OperatorWord */ .chroma .ow { color: #f92672 } 124 | /* Comment */ .chroma .c { color: #75715e } 125 | /* CommentHashbang */ .chroma .ch { color: #75715e } 126 | /* CommentMultiline */ .chroma .cm { color: #75715e } 127 | /* CommentSingle */ .chroma .c1 { color: #75715e } 128 | /* CommentSpecial */ .chroma .cs { color: #75715e } 129 | /* CommentPreproc */ .chroma .cp { color: #75715e } 130 | /* CommentPreprocFile */ .chroma .cpf { color: #75715e } 131 | /* GenericDeleted */ .chroma .gd { color: #f92672 } 132 | /* GenericEmph */ .chroma .ge { font-style: italic } 133 | /* GenericInserted */ .chroma .gi { color: #a6e22e } 134 | /* GenericStrong */ .chroma .gs { font-weight: bold } 135 | /* GenericSubheading */ .chroma .gu { color: #75715e } 136 | } 137 | 138 | 139 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/assets/sass/layout.sass: -------------------------------------------------------------------------------- 1 | *:root 2 | --content-margin: 6vw 3 | 4 | @media (min-width: $wider-display) 5 | *:root 6 | --content-margin: 70px 7 | 8 | html, body 9 | margin: 0 10 | width: 100% 11 | height: 100% 12 | background-color: var(--bgbg) 13 | color: var(--font-color) 14 | 15 | #wrap 16 | min-height: 100vh 17 | display: flex 18 | flex-direction: column 19 | justify-content: space-between 20 | align-items: center 21 | 22 | header, footer 23 | display: flex 24 | justify-content: center 25 | align-items: center 26 | width: 100% 27 | min-height: 60px 28 | background-color: var(--accent) 29 | 30 | #main 31 | width: 83.2ex // <-- ~80 chars 32 | max-width: 100% 33 | background-color: var(--bg) 34 | 35 | @media (min-width: $page-width) 36 | box-shadow: 0px 6px 12px 3px var(--shadow-color) 37 | 38 | > :first-child 39 | margin: 0 var(--content-margin) 40 | padding: 46px 0 41 | 42 | @media (min-width: $page-width) 43 | margin-top: 23px 44 | margin-bottom: 23px 45 | 46 | blockquote 47 | margin-left: 2px 48 | margin-right: 6px 49 | padding-left: 16px 50 | border-left: 3px solid var(--blockquote-accent) 51 | color: var(--blockquote) 52 | 53 | pre 54 | padding: 6px 8px 55 | 56 | img 57 | margin: 0.5em auto 58 | width: calc(100% + 2 * var(--content-margin)) 59 | margin-inline-start: calc(-1 * var(--content-margin)) 60 | 61 | display: block 62 | height: auto 63 | 64 | border: 1px solid var(--accent) 65 | border-left: none 66 | border-right: none 67 | 68 | hr 69 | border: 0; 70 | height: 1px; 71 | background-image: linear-gradient(to right, rgba(0, 0, 0, 0), var(--accent), rgba(0, 0, 0, 0)); 72 | 73 | .aftermath 74 | text-align: center 75 | 76 | time 77 | font-weight: 100 78 | opacity: .3 79 | 80 | nav 81 | display: inline-block 82 | font-size: 1.4em 83 | 84 | ul 85 | display: inline-block 86 | list-style-type: none 87 | margin: 0 88 | padding: 0 89 | 90 | li 91 | display: inline-block 92 | 93 | &:not(:first-child) 94 | margin-left: 14px 95 | 96 | .posts-list 97 | .post 98 | margin-top: 44px 99 | 100 | &:last-of-type 101 | hr 102 | display: none 103 | 104 | .title 105 | width: 90% 106 | margin: 0 auto 107 | font-size: 1.25em 108 | font-weight: semibold 109 | 110 | time 111 | margin-top: 32px 112 | display: block 113 | text-align: center 114 | 115 | 116 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/assets/sass/main.sass: -------------------------------------------------------------------------------- 1 | @import ../vendor/css/modern-normalize 2 | 3 | @import vars 4 | @import layout 5 | @import readability 6 | @import code 7 | 8 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/assets/sass/readability.sass: -------------------------------------------------------------------------------- 1 | body 2 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol 3 | text-rendering: optimizeLegibility 4 | text-align: start 5 | word-wrap: break-word 6 | --webkit-font-smoothing: subpixel-antialiased 7 | 8 | header 9 | a 10 | color: white 11 | text-decoration: none 12 | 13 | nav 14 | a 15 | color: var(--realbg) 16 | text-decoration: none 17 | 18 | #main 19 | font-size: 18px 20 | line-height: 29px 21 | 22 | h1 23 | font-size: 1.5em 24 | 25 | h2 26 | font-size: 1.5em 27 | 28 | h3 29 | font-size: 1.25em 30 | 31 | h4, h5, h6 32 | font-size: 1em 33 | margin: 1em 0 34 | 35 | a 36 | color: var(--accent-bright) 37 | text-decoration: none 38 | font-weight: 400 39 | 40 | &:hover 41 | text-decoration: underline 42 | text-decoration-style: dotted 43 | 44 | article 45 | h1:first-of-type 46 | margin-top: 0 47 | margin-bottom: 0.5em 48 | font-weight: bold 49 | font-size: 1.95552em 50 | line-height: 1.2141em 51 | 52 | h2:first-of-type 53 | color: var(--subhead-color) 54 | font-size: 1.46664em 55 | margin-top: -0.35em 56 | line-height: 1.27275em 57 | font-weight: normal 58 | 59 | 60 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/assets/sass/vars.sass: -------------------------------------------------------------------------------- 1 | $page-width: 800px 2 | $wider-display: 500px 3 | 4 | $accent: rgb(183, 64, 233) 5 | 6 | *:root 7 | --realbg: rgb(230, 230, 230) 8 | --bgbg: rgb(255, 255, 255) 9 | --bg: rgb(255, 255, 255) 10 | --accent: #{$accent} 11 | --accent-bright: #{$accent} 12 | --accent-light: #{lighten($accent, 20%)} 13 | --font-color: rgb(27, 27, 27) 14 | --subhead-color: rgba(27, 27, 27, 0.65) 15 | --shadow-color: rgba(0, 0, 0, 0.2) 16 | --blockquote: rgba(0, 0, 0, 0.65) 17 | --blockquote-accent: rgba(0, 0, 0, 0.1) 18 | 19 | @media (min-width: $page-width) 20 | *:root 21 | --bgbg: rgb(230, 230, 230) 22 | 23 | @media (prefers-color-scheme: dark) 24 | *:root 25 | --realbg: rgb(50, 50, 51) 26 | --bgbg: rgb(74, 74, 77) 27 | --bg: rgb(74, 74, 77) 28 | --accent: #{darken($accent, 28%)} 29 | --accent-bright: #{lighten($accent, 20%)} 30 | --accent-light: #{lighten($accent, 30%)} 31 | --font-color: rgba(255, 255, 255, 0.78) 32 | --subhead-color: rgba(255, 255, 255, 0.65) 33 | --blockquote: rgba(255, 255, 255, 0.6) 34 | --blockquote-accent: rgba(255, 255, 255, 0.15) 35 | 36 | @media (prefers-color-scheme: dark) and (min-width: $page-width) 37 | *:root 38 | --bgbg: rgb(50, 50, 51) 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/assets/vendor/css/modern-normalize.css: -------------------------------------------------------------------------------- 1 | /*! modern-normalize | MIT License | https://github.com/sindresorhus/modern-normalize */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * Use a better box model (opinionated). 8 | */ 9 | 10 | html { 11 | box-sizing: border-box; 12 | } 13 | 14 | *, 15 | *::before, 16 | *::after { 17 | box-sizing: inherit; 18 | } 19 | 20 | /** 21 | * Use a more readable tab size (opinionated). 22 | */ 23 | 24 | :root { 25 | -moz-tab-size: 4; 26 | tab-size: 4; 27 | } 28 | 29 | /** 30 | * 1. Correct the line height in all browsers. 31 | * 2. Prevent adjustments of font size after orientation changes in iOS. 32 | */ 33 | 34 | html { 35 | line-height: 1.15; /* 1 */ 36 | -webkit-text-size-adjust: 100%; /* 2 */ 37 | } 38 | 39 | /* Sections 40 | ========================================================================== */ 41 | 42 | /** 43 | * Remove the margin in all browsers. 44 | */ 45 | 46 | body { 47 | margin: 0; 48 | } 49 | 50 | /** 51 | * Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) 52 | */ 53 | 54 | body { 55 | font-family: 56 | -apple-system, 57 | BlinkMacSystemFont, 58 | 'Segoe UI', 59 | Roboto, 60 | Helvetica, 61 | Arial, 62 | sans-serif, 63 | 'Apple Color Emoji', 64 | 'Segoe UI Emoji', 65 | 'Segoe UI Symbol'; 66 | } 67 | 68 | /* Grouping content 69 | ========================================================================== */ 70 | 71 | /** 72 | * Add the correct height in Firefox. 73 | */ 74 | 75 | hr { 76 | height: 0; 77 | } 78 | 79 | /* Text-level semantics 80 | ========================================================================== */ 81 | 82 | /** 83 | * Add the correct text decoration in Chrome, Edge, and Safari. 84 | */ 85 | 86 | abbr[title] { 87 | text-decoration: underline dotted; 88 | } 89 | 90 | /** 91 | * Add the correct font weight in Chrome, Edge, and Safari. 92 | */ 93 | 94 | b, 95 | strong { 96 | font-weight: bolder; 97 | } 98 | 99 | /** 100 | * 1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) 101 | * 2. Correct the odd `em` font sizing in all browsers. 102 | */ 103 | 104 | code, 105 | kbd, 106 | samp, 107 | pre { 108 | font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in all browsers. 122 | */ 123 | 124 | sub, 125 | sup { 126 | font-size: 75%; 127 | line-height: 0; 128 | position: relative; 129 | vertical-align: baseline; 130 | } 131 | 132 | sub { 133 | bottom: -0.25em; 134 | } 135 | 136 | sup { 137 | top: -0.5em; 138 | } 139 | 140 | /* Forms 141 | ========================================================================== */ 142 | 143 | /** 144 | * 1. Change the font styles in all browsers. 145 | * 2. Remove the margin in Firefox and Safari. 146 | */ 147 | 148 | button, 149 | input, 150 | optgroup, 151 | select, 152 | textarea { 153 | font-family: inherit; /* 1 */ 154 | font-size: 100%; /* 1 */ 155 | line-height: 1.15; /* 1 */ 156 | margin: 0; /* 2 */ 157 | } 158 | 159 | /** 160 | * Remove the inheritance of text transform in Edge and Firefox. 161 | * 1. Remove the inheritance of text transform in Firefox. 162 | */ 163 | 164 | button, 165 | select { /* 1 */ 166 | text-transform: none; 167 | } 168 | 169 | /** 170 | * Correct the inability to style clickable types in iOS and Safari. 171 | */ 172 | 173 | button, 174 | [type='button'], 175 | [type='reset'], 176 | [type='submit'] { 177 | -webkit-appearance: button; 178 | } 179 | 180 | /** 181 | * Remove the inner border and padding in Firefox. 182 | */ 183 | 184 | button::-moz-focus-inner, 185 | [type='button']::-moz-focus-inner, 186 | [type='reset']::-moz-focus-inner, 187 | [type='submit']::-moz-focus-inner { 188 | border-style: none; 189 | padding: 0; 190 | } 191 | 192 | /** 193 | * Restore the focus styles unset by the previous rule. 194 | */ 195 | 196 | button:-moz-focusring, 197 | [type='button']:-moz-focusring, 198 | [type='reset']:-moz-focusring, 199 | [type='submit']:-moz-focusring { 200 | outline: 1px dotted ButtonText; 201 | } 202 | 203 | /** 204 | * Correct the padding in Firefox. 205 | */ 206 | 207 | fieldset { 208 | padding: 0.35em 0.75em 0.625em; 209 | } 210 | 211 | /** 212 | * Remove the padding so developers are not caught out when they zero out `fieldset` elements in all browsers. 213 | */ 214 | 215 | legend { 216 | padding: 0; 217 | } 218 | 219 | /** 220 | * Add the correct vertical alignment in Chrome and Firefox. 221 | */ 222 | 223 | progress { 224 | vertical-align: baseline; 225 | } 226 | 227 | /** 228 | * Correct the cursor style of increment and decrement buttons in Safari. 229 | */ 230 | 231 | [type='number']::-webkit-inner-spin-button, 232 | [type='number']::-webkit-outer-spin-button { 233 | height: auto; 234 | } 235 | 236 | /** 237 | * 1. Correct the odd appearance in Chrome and Safari. 238 | * 2. Correct the outline style in Safari. 239 | */ 240 | 241 | [type='search'] { 242 | -webkit-appearance: textfield; /* 1 */ 243 | outline-offset: -2px; /* 2 */ 244 | } 245 | 246 | /** 247 | * Remove the inner padding in Chrome and Safari on macOS. 248 | */ 249 | 250 | [type='search']::-webkit-search-decoration { 251 | -webkit-appearance: none; 252 | } 253 | 254 | /** 255 | * 1. Correct the inability to style clickable types in iOS and Safari. 256 | * 2. Change font properties to `inherit` in Safari. 257 | */ 258 | 259 | ::-webkit-file-upload-button { 260 | -webkit-appearance: button; /* 1 */ 261 | font: inherit; /* 2 */ 262 | } 263 | 264 | /* Interactive 265 | ========================================================================== */ 266 | 267 | /* 268 | * Add the correct display in Chrome and Safari. 269 | */ 270 | 271 | summary { 272 | display: list-item; 273 | } 274 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/layouts/404.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/layouts/_default/baseof.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{- partial "head.html" . -}} 4 | 5 |
6 | {{- partial "header.html" . -}} 7 |
8 | {{- block "main" . }}{{- end }} 9 |
10 | {{- partial "footer.html" . -}} 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/layouts/_default/list.html: -------------------------------------------------------------------------------- 1 | {{ define "main" }} 2 |
3 |
4 | {{- .Content -}} 5 |
6 |
7 | {{- partial "posts-list.html" . -}} 8 |
9 |
10 | {{ end }} 11 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/layouts/_default/single.html: -------------------------------------------------------------------------------- 1 | {{ define "main" }} 2 |
3 |

{{ .Title }}

4 | {{ .Content }} 5 |
6 |
7 | {{ if .Date }} 8 | 11 | {{ end }} 12 |
13 |
14 | {{ end }} 15 | 16 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/layouts/index.html: -------------------------------------------------------------------------------- 1 | {{ define "main" }} 2 |
3 |
4 | {{- .Content -}} 5 |
6 |
7 | {{ with .Site.GetPage "/posts" }} 8 | {{- partial "posts-list.html" . -}} 9 | {{ end }} 10 |
11 |
12 | {{ end }} 13 | 14 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/layouts/index.rss.xml: -------------------------------------------------------------------------------- 1 | {{ printf "" | safeHTML }} 2 | 3 | 4 | {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }} 5 | {{ .Permalink }} 6 | Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }} 7 | Hugo -- gohugo.io{{ with .Site.LanguageCode }} 8 | {{.}}{{end}}{{ with .Site.Author.email }} 9 | {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Author.email }} 10 | {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Copyright }} 11 | {{.}}{{end}}{{ if not .Date.IsZero }} 12 | {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}{{ end }} 13 | {{ with .OutputFormats.Get "RSS" }} 14 | {{ printf "" .Permalink .MediaType | safeHTML }} 15 | {{ end }} 16 | {{ range .Pages }} 17 | 18 | {{ .Title }} 19 | {{ .Permalink }} 20 | {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }} 21 | {{ with .Site.Author.email }}{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}} 22 | {{ .Permalink }} 23 | {{ .Content | html }} 24 | 25 | {{ end }} 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/layouts/partials/css.html: -------------------------------------------------------------------------------- 1 | {{ $bundleRaw := resources.Get "/sass/main.sass" | resources.ExecuteAsTemplate "/css/main.tmp.css" . }} 2 | 3 | {{ $cssOpts := (dict "targetPath" "/css/main.css" "enableSourceMap" true ) }} 4 | {{ $bundle := $bundleRaw | toCSS $cssOpts | minify | fingerprint }} 5 | 6 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/layouts/partials/footer.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/layouts/partials/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ .Title }} 5 | {{ partial "social-preview.html" . }} 6 | {{ partial "css.html" . }} 7 | {{ range .AlternativeOutputFormats -}} 8 | {{ printf `` .Rel .MediaType.Type .RelPermalink $.Site.Title | safeHTML }} 9 | {{ end -}} 10 | 11 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/layouts/partials/header.html: -------------------------------------------------------------------------------- 1 |
2 |

{{.Site.Title}}

3 |
4 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/layouts/partials/js.html: -------------------------------------------------------------------------------- 1 | {{ $index := resources.Get "/js/index.js" | minify }} 2 | {{ $scripts := slice $index | resources.Concat "/js/bundle.js" | fingerprint }} 3 | 4 | 5 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/layouts/partials/posts-list.html: -------------------------------------------------------------------------------- 1 |
2 | {{ range .Pages.ByPublishDate.Reverse }} 3 |
4 |
5 | {{ .Title }} 6 |
7 | 10 |
11 |
12 | {{ end }} 13 |
14 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/layouts/partials/social-preview.html: -------------------------------------------------------------------------------- 1 | {{ $description := .Params.Description | default "A thing @searls wrote for you." }} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/static/blog/themes/ultralight/theme.toml: -------------------------------------------------------------------------------- 1 | # theme.toml template for a Hugo theme 2 | # See https://github.com/gohugoio/hugoThemes#themetoml for an example 3 | 4 | name = "Ultralight" 5 | license = "Unlicensed" 6 | description = "Private" 7 | homepage = "http://github.com/searls/hugo-theme-ultralight" 8 | tags = [] 9 | features = [] 10 | min_version = "0.54" 11 | 12 | [author] 13 | name = "Justin Searls" 14 | homepage = "https://twitter.com/searls" 15 | -------------------------------------------------------------------------------- /example/static/docs/.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_PATH: "vendor/bundle" 3 | -------------------------------------------------------------------------------- /example/static/docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-cache 4 | .jekyll-metadata 5 | vendor 6 | -------------------------------------------------------------------------------- /example/static/docs/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /404.html 3 | layout: default 4 | --- 5 | 6 | 19 | 20 |
21 |

404

22 | 23 |

Page not found :(

24 |

The requested page could not be found.

25 |
26 | -------------------------------------------------------------------------------- /example/static/docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "jekyll" 4 | gem "minima" 5 | 6 | group :jekyll_plugins do 7 | gem "jekyll-feed" 8 | end 9 | 10 | install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do 11 | gem "tzinfo" 12 | gem "tzinfo-data" 13 | end 14 | 15 | gem "wdm", "~> 0.1.1", :install_if => Gem.win_platform? 16 | 17 | -------------------------------------------------------------------------------- /example/static/docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.0) 5 | public_suffix (>= 2.0.2, < 5.0) 6 | colorator (1.1.0) 7 | concurrent-ruby (1.1.9) 8 | em-websocket (0.5.2) 9 | eventmachine (>= 0.12.9) 10 | http_parser.rb (~> 0.6.0) 11 | eventmachine (1.2.7) 12 | ffi (1.15.3) 13 | forwardable-extended (2.6.0) 14 | http_parser.rb (0.6.0) 15 | i18n (1.8.10) 16 | concurrent-ruby (~> 1.0) 17 | jekyll (4.2.0) 18 | addressable (~> 2.4) 19 | colorator (~> 1.0) 20 | em-websocket (~> 0.5) 21 | i18n (~> 1.0) 22 | jekyll-sass-converter (~> 2.0) 23 | jekyll-watch (~> 2.0) 24 | kramdown (~> 2.3) 25 | kramdown-parser-gfm (~> 1.0) 26 | liquid (~> 4.0) 27 | mercenary (~> 0.4.0) 28 | pathutil (~> 0.9) 29 | rouge (~> 3.0) 30 | safe_yaml (~> 1.0) 31 | terminal-table (~> 2.0) 32 | jekyll-feed (0.15.1) 33 | jekyll (>= 3.7, < 5.0) 34 | jekyll-sass-converter (2.1.0) 35 | sassc (> 2.0.1, < 3.0) 36 | jekyll-seo-tag (2.7.1) 37 | jekyll (>= 3.8, < 5.0) 38 | jekyll-watch (2.2.1) 39 | listen (~> 3.0) 40 | kramdown (2.3.1) 41 | rexml 42 | kramdown-parser-gfm (1.1.0) 43 | kramdown (~> 2.0) 44 | liquid (4.0.3) 45 | listen (3.6.0) 46 | rb-fsevent (~> 0.10, >= 0.10.3) 47 | rb-inotify (~> 0.9, >= 0.9.10) 48 | mercenary (0.4.0) 49 | minima (2.5.1) 50 | jekyll (>= 3.5, < 5.0) 51 | jekyll-feed (~> 0.9) 52 | jekyll-seo-tag (~> 2.1) 53 | pathutil (0.16.2) 54 | forwardable-extended (~> 2.6) 55 | public_suffix (4.0.6) 56 | rb-fsevent (0.11.0) 57 | rb-inotify (0.10.1) 58 | ffi (~> 1.0) 59 | rexml (3.2.5) 60 | rouge (3.26.0) 61 | safe_yaml (1.0.5) 62 | sassc (2.4.0) 63 | ffi (~> 1.9) 64 | terminal-table (2.0.0) 65 | unicode-display_width (~> 1.1, >= 1.1.1) 66 | tzinfo (2.0.4) 67 | concurrent-ruby (~> 1.0) 68 | tzinfo-data (1.2021.1) 69 | tzinfo (>= 1.0.0) 70 | unicode-display_width (1.7.0) 71 | wdm (0.1.1) 72 | 73 | PLATFORMS 74 | ruby 75 | 76 | DEPENDENCIES 77 | jekyll 78 | jekyll-feed 79 | minima 80 | tzinfo 81 | tzinfo-data 82 | wdm (~> 0.1.1) 83 | 84 | BUNDLED WITH 85 | 2.2.15 86 | -------------------------------------------------------------------------------- /example/static/docs/_config.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Jekyll! 2 | # 3 | # This config file is meant for settings that affect your whole blog, values 4 | # which you are expected to set up once and rarely edit after that. If you find 5 | # yourself editing this file very often, consider using Jekyll's data files 6 | # feature for the data you need to update frequently. 7 | # 8 | # For technical reasons, this file is *NOT* reloaded automatically when you use 9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process. 10 | # 11 | # If you need help with YAML syntax, here are some quick references for you: 12 | # https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml 13 | # https://learnxinyminutes.com/docs/yaml/ 14 | # 15 | # Site settings 16 | # These are used to personalize your new site. If you look in the HTML files, 17 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. 18 | # You can create any custom variable you would like, and they will be accessible 19 | # in the templates via {{ site.myvariable }}. 20 | 21 | title: Your awesome title 22 | email: your-email@example.com 23 | description: >- # this means to ignore newlines until "baseurl:" 24 | Write an awesome description for your new site here. You can edit this 25 | line in _config.yml. It will appear in your document head meta (for 26 | Google search results) and in your feed.xml site description. 27 | baseurl: "/docs" # the subpath of your site, e.g. /blog 28 | url: "" # the base hostname & protocol for your site, e.g. http://example.com 29 | twitter_username: jekyllrb 30 | github_username: jekyll 31 | 32 | # Build settings 33 | theme: minima 34 | plugins: 35 | - jekyll-feed 36 | 37 | # Exclude from processing. 38 | # The following items will not be processed, by default. 39 | # Any item listed under the `exclude:` key here will be automatically added to 40 | # the internal "default list". 41 | # 42 | # Excluded items can be processed by explicitly listing the directories or 43 | # their entries' file path in the `include:` list. 44 | # 45 | # exclude: 46 | # - .sass-cache/ 47 | # - .jekyll-cache/ 48 | # - gemfiles/ 49 | # - Gemfile 50 | # - Gemfile.lock 51 | # - node_modules/ 52 | # - vendor/bundle/ 53 | # - vendor/cache/ 54 | # - vendor/gems/ 55 | # - vendor/ruby/ 56 | -------------------------------------------------------------------------------- /example/static/docs/_posts/2020-04-27-welcome-to-jekyll.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Welcome to Jekyll!" 4 | date: 2020-04-27 15:21:07 +0900 5 | categories: jekyll update 6 | --- 7 | You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated. 8 | 9 | Jekyll requires blog post files to be named according to the following format: 10 | 11 | `YEAR-MONTH-DAY-title.MARKUP` 12 | 13 | Where `YEAR` is a four-digit number, `MONTH` and `DAY` are both two-digit numbers, and `MARKUP` is the file extension representing the format used in the file. After that, include the necessary front matter. Take a look at the source for this post to get an idea about how it works. 14 | 15 | Jekyll also offers powerful support for code snippets: 16 | 17 | {% highlight ruby %} 18 | def print_hi(name) 19 | puts "Hi, #{name}" 20 | end 21 | print_hi('Tom') 22 | #=> prints 'Hi, Tom' to STDOUT. 23 | {% endhighlight %} 24 | 25 | Check out the [Jekyll docs][jekyll-docs] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll Talk][jekyll-talk]. 26 | 27 | [jekyll-docs]: https://jekyllrb.com/docs/home 28 | [jekyll-gh]: https://github.com/jekyll/jekyll 29 | [jekyll-talk]: https://talk.jekyllrb.com/ 30 | -------------------------------------------------------------------------------- /example/static/docs/about.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: About 4 | permalink: /about/ 5 | --- 6 | 7 | This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](https://jekyllrb.com/) 8 | 9 | You can find the source code for Minima at GitHub: 10 | [jekyll][jekyll-organization] / 11 | [minima](https://github.com/jekyll/minima) 12 | 13 | You can find the source code for Jekyll at GitHub: 14 | [jekyll][jekyll-organization] / 15 | [jekyll](https://github.com/jekyll/jekyll) 16 | 17 | 18 | [jekyll-organization]: https://github.com/jekyll 19 | -------------------------------------------------------------------------------- /example/static/docs/assets/a_css.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: blue 3 | } 4 | -------------------------------------------------------------------------------- /example/static/docs/assets/a_css.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/static/docs/assets/a_css.css.gz -------------------------------------------------------------------------------- /example/static/docs/assets/a_html.html: -------------------------------------------------------------------------------- 1 | Neat 2 | -------------------------------------------------------------------------------- /example/static/docs/assets/a_html.html.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/static/docs/assets/a_html.html.gz -------------------------------------------------------------------------------- /example/static/docs/assets/a_javascript.js: -------------------------------------------------------------------------------- 1 | console.log('hello'); 2 | -------------------------------------------------------------------------------- /example/static/docs/assets/a_javascript.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/static/docs/assets/a_javascript.js.gz -------------------------------------------------------------------------------- /example/static/docs/assets/a_json.json: -------------------------------------------------------------------------------- 1 | { 2 | "stuff": ["things"] 3 | } 4 | -------------------------------------------------------------------------------- /example/static/docs/assets/a_json.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/static/docs/assets/a_json.json.gz -------------------------------------------------------------------------------- /example/static/docs/assets/an_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/static/docs/assets/an_image.png -------------------------------------------------------------------------------- /example/static/docs/assets/an_image.png.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/static/docs/assets/an_image.png.gz -------------------------------------------------------------------------------- /example/static/docs/index.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | # Feel free to add content and custom Front Matter to this file. 3 | # To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults 4 | 5 | layout: home 6 | --- 7 | -------------------------------------------------------------------------------- /example/static/marketing/.gitignore: -------------------------------------------------------------------------------- 1 | /resources 2 | /public 3 | -------------------------------------------------------------------------------- /example/static/marketing/archetypes/default.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "{{ replace .Name "-" " " | title }}" 3 | date: {{ .Date }} 4 | draft: true 5 | --- 6 | 7 | -------------------------------------------------------------------------------- /example/static/marketing/config.toml: -------------------------------------------------------------------------------- 1 | baseURL = "http://example.com/marketing" 2 | languageCode = "en-us" 3 | title = "My Marketing Page" 4 | theme = "ultralight" 5 | -------------------------------------------------------------------------------- /example/static/marketing/content/posts/my-first-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "My First Post" 3 | date: 2020-04-29T16:49:04+09:00 4 | --- 5 | h2 Hello 6 | 7 | I am a post 8 | 9 | -------------------------------------------------------------------------------- /example/static/marketing/layouts/404.html: -------------------------------------------------------------------------------- 1 |
2 | I am a 404 page for /marketing 3 |
4 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/archetypes/default.md: -------------------------------------------------------------------------------- 1 | +++ 2 | +++ 3 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/assets/js/index.js: -------------------------------------------------------------------------------- 1 | // JS can go here. 2 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/assets/sass/code.css: -------------------------------------------------------------------------------- 1 | /* LIGHT MODE */ 2 | /* hugo gen chromastyles --style autumn */ 3 | 4 | /* Background */ .chroma { background-color: #ffffff } 5 | /* Error */ .chroma .err { color: #ff0000; background-color: #ffaaaa } 6 | /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } 7 | /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; } 8 | /* LineHighlight */ .chroma .hl { display: block; width: 100%;background-color: #ffffcc } 9 | /* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } 10 | /* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } 11 | /* Keyword */ .chroma .k { color: #0000aa } 12 | /* KeywordConstant */ .chroma .kc { color: #0000aa } 13 | /* KeywordDeclaration */ .chroma .kd { color: #0000aa } 14 | /* KeywordNamespace */ .chroma .kn { color: #0000aa } 15 | /* KeywordPseudo */ .chroma .kp { color: #0000aa } 16 | /* KeywordReserved */ .chroma .kr { color: #0000aa } 17 | /* KeywordType */ .chroma .kt { color: #00aaaa } 18 | /* NameAttribute */ .chroma .na { color: #1e90ff } 19 | /* NameBuiltin */ .chroma .nb { color: #00aaaa } 20 | /* NameClass */ .chroma .nc { color: #00aa00; text-decoration: underline } 21 | /* NameConstant */ .chroma .no { color: #aa0000 } 22 | /* NameDecorator */ .chroma .nd { color: #888888 } 23 | /* NameEntity */ .chroma .ni { color: #880000; font-weight: bold } 24 | /* NameFunction */ .chroma .nf { color: #00aa00 } 25 | /* NameNamespace */ .chroma .nn { color: #00aaaa; text-decoration: underline } 26 | /* NameTag */ .chroma .nt { color: #1e90ff; font-weight: bold } 27 | /* NameVariable */ .chroma .nv { color: #aa0000 } 28 | /* LiteralString */ .chroma .s { color: #aa5500 } 29 | /* LiteralStringAffix */ .chroma .sa { color: #aa5500 } 30 | /* LiteralStringBacktick */ .chroma .sb { color: #aa5500 } 31 | /* LiteralStringChar */ .chroma .sc { color: #aa5500 } 32 | /* LiteralStringDelimiter */ .chroma .dl { color: #aa5500 } 33 | /* LiteralStringDoc */ .chroma .sd { color: #aa5500 } 34 | /* LiteralStringDouble */ .chroma .s2 { color: #aa5500 } 35 | /* LiteralStringEscape */ .chroma .se { color: #aa5500 } 36 | /* LiteralStringHeredoc */ .chroma .sh { color: #aa5500 } 37 | /* LiteralStringInterpol */ .chroma .si { color: #aa5500 } 38 | /* LiteralStringOther */ .chroma .sx { color: #aa5500 } 39 | /* LiteralStringRegex */ .chroma .sr { color: #009999 } 40 | /* LiteralStringSingle */ .chroma .s1 { color: #aa5500 } 41 | /* LiteralStringSymbol */ .chroma .ss { color: #0000aa } 42 | /* LiteralNumber */ .chroma .m { color: #009999 } 43 | /* LiteralNumberBin */ .chroma .mb { color: #009999 } 44 | /* LiteralNumberFloat */ .chroma .mf { color: #009999 } 45 | /* LiteralNumberHex */ .chroma .mh { color: #009999 } 46 | /* LiteralNumberInteger */ .chroma .mi { color: #009999 } 47 | /* LiteralNumberIntegerLong */ .chroma .il { color: #009999 } 48 | /* LiteralNumberOct */ .chroma .mo { color: #009999 } 49 | /* OperatorWord */ .chroma .ow { color: #0000aa } 50 | /* Comment */ .chroma .c { color: #aaaaaa; font-style: italic } 51 | /* CommentHashbang */ .chroma .ch { color: #aaaaaa; font-style: italic } 52 | /* CommentMultiline */ .chroma .cm { color: #aaaaaa; font-style: italic } 53 | /* CommentSingle */ .chroma .c1 { color: #aaaaaa; font-style: italic } 54 | /* CommentSpecial */ .chroma .cs { color: #0000aa; font-style: italic } 55 | /* CommentPreproc */ .chroma .cp { color: #4c8317 } 56 | /* CommentPreprocFile */ .chroma .cpf { color: #4c8317 } 57 | /* GenericDeleted */ .chroma .gd { color: #aa0000 } 58 | /* GenericEmph */ .chroma .ge { font-style: italic } 59 | /* GenericError */ .chroma .gr { color: #aa0000 } 60 | /* GenericHeading */ .chroma .gh { color: #000080; font-weight: bold } 61 | /* GenericInserted */ .chroma .gi { color: #00aa00 } 62 | /* GenericOutput */ .chroma .go { color: #888888 } 63 | /* GenericPrompt */ .chroma .gp { color: #555555 } 64 | /* GenericStrong */ .chroma .gs { font-weight: bold } 65 | /* GenericSubheading */ .chroma .gu { color: #800080; font-weight: bold } 66 | /* GenericTraceback */ .chroma .gt { color: #aa0000 } 67 | /* GenericUnderline */ .chroma .gl { text-decoration: underline } 68 | /* TextWhitespace */ .chroma .w { color: #bbbbbb } 69 | 70 | /* LIGHT MODE OVERRIDES: */ 71 | /* Background */ .chroma { background-color: #f6f6f6 } 72 | 73 | 74 | /* DARK MODE */ 75 | @media (prefers-color-scheme: dark) { 76 | /* hugo gen chromastyles --style monokai */ 77 | /* Background */ .chroma { color: #f8f8f2; background-color: #272822 } 78 | /* Error */ .chroma .err { color: #960050; background-color: #1e0010 } 79 | /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } 80 | /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; } 81 | /* LineHighlight */ .chroma .hl { display: block; width: 100%;background-color: #ffffcc } 82 | /* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } 83 | /* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } 84 | /* Keyword */ .chroma .k { color: #66d9ef } 85 | /* KeywordConstant */ .chroma .kc { color: #66d9ef } 86 | /* KeywordDeclaration */ .chroma .kd { color: #66d9ef } 87 | /* KeywordNamespace */ .chroma .kn { color: #f92672 } 88 | /* KeywordPseudo */ .chroma .kp { color: #66d9ef } 89 | /* KeywordReserved */ .chroma .kr { color: #66d9ef } 90 | /* KeywordType */ .chroma .kt { color: #66d9ef } 91 | /* NameAttribute */ .chroma .na { color: #a6e22e } 92 | /* NameClass */ .chroma .nc { color: #a6e22e } 93 | /* NameConstant */ .chroma .no { color: #66d9ef } 94 | /* NameDecorator */ .chroma .nd { color: #a6e22e } 95 | /* NameException */ .chroma .ne { color: #a6e22e } 96 | /* NameFunction */ .chroma .nf { color: #a6e22e } 97 | /* NameOther */ .chroma .nx { color: #a6e22e } 98 | /* NameTag */ .chroma .nt { color: #f92672 } 99 | /* Literal */ .chroma .l { color: #ae81ff } 100 | /* LiteralDate */ .chroma .ld { color: #e6db74 } 101 | /* LiteralString */ .chroma .s { color: #e6db74 } 102 | /* LiteralStringAffix */ .chroma .sa { color: #e6db74 } 103 | /* LiteralStringBacktick */ .chroma .sb { color: #e6db74 } 104 | /* LiteralStringChar */ .chroma .sc { color: #e6db74 } 105 | /* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 } 106 | /* LiteralStringDoc */ .chroma .sd { color: #e6db74 } 107 | /* LiteralStringDouble */ .chroma .s2 { color: #e6db74 } 108 | /* LiteralStringEscape */ .chroma .se { color: #ae81ff } 109 | /* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 } 110 | /* LiteralStringInterpol */ .chroma .si { color: #e6db74 } 111 | /* LiteralStringOther */ .chroma .sx { color: #e6db74 } 112 | /* LiteralStringRegex */ .chroma .sr { color: #e6db74 } 113 | /* LiteralStringSingle */ .chroma .s1 { color: #e6db74 } 114 | /* LiteralStringSymbol */ .chroma .ss { color: #e6db74 } 115 | /* LiteralNumber */ .chroma .m { color: #ae81ff } 116 | /* LiteralNumberBin */ .chroma .mb { color: #ae81ff } 117 | /* LiteralNumberFloat */ .chroma .mf { color: #ae81ff } 118 | /* LiteralNumberHex */ .chroma .mh { color: #ae81ff } 119 | /* LiteralNumberInteger */ .chroma .mi { color: #ae81ff } 120 | /* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff } 121 | /* LiteralNumberOct */ .chroma .mo { color: #ae81ff } 122 | /* Operator */ .chroma .o { color: #f92672 } 123 | /* OperatorWord */ .chroma .ow { color: #f92672 } 124 | /* Comment */ .chroma .c { color: #75715e } 125 | /* CommentHashbang */ .chroma .ch { color: #75715e } 126 | /* CommentMultiline */ .chroma .cm { color: #75715e } 127 | /* CommentSingle */ .chroma .c1 { color: #75715e } 128 | /* CommentSpecial */ .chroma .cs { color: #75715e } 129 | /* CommentPreproc */ .chroma .cp { color: #75715e } 130 | /* CommentPreprocFile */ .chroma .cpf { color: #75715e } 131 | /* GenericDeleted */ .chroma .gd { color: #f92672 } 132 | /* GenericEmph */ .chroma .ge { font-style: italic } 133 | /* GenericInserted */ .chroma .gi { color: #a6e22e } 134 | /* GenericStrong */ .chroma .gs { font-weight: bold } 135 | /* GenericSubheading */ .chroma .gu { color: #75715e } 136 | } 137 | 138 | 139 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/assets/sass/layout.sass: -------------------------------------------------------------------------------- 1 | *:root 2 | --content-margin: 6vw 3 | 4 | @media (min-width: $wider-display) 5 | *:root 6 | --content-margin: 70px 7 | 8 | html, body 9 | margin: 0 10 | width: 100% 11 | height: 100% 12 | background-color: var(--bgbg) 13 | color: var(--font-color) 14 | 15 | #wrap 16 | min-height: 100vh 17 | display: flex 18 | flex-direction: column 19 | justify-content: space-between 20 | align-items: center 21 | 22 | header, footer 23 | display: flex 24 | justify-content: center 25 | align-items: center 26 | width: 100% 27 | min-height: 60px 28 | background-color: var(--accent) 29 | 30 | #main 31 | width: 83.2ex // <-- ~80 chars 32 | max-width: 100% 33 | background-color: var(--bg) 34 | 35 | @media (min-width: $page-width) 36 | box-shadow: 0px 6px 12px 3px var(--shadow-color) 37 | 38 | > :first-child 39 | margin: 0 var(--content-margin) 40 | padding: 46px 0 41 | 42 | @media (min-width: $page-width) 43 | margin-top: 23px 44 | margin-bottom: 23px 45 | 46 | blockquote 47 | margin-left: 2px 48 | margin-right: 6px 49 | padding-left: 16px 50 | border-left: 3px solid var(--blockquote-accent) 51 | color: var(--blockquote) 52 | 53 | pre 54 | padding: 6px 8px 55 | 56 | img 57 | margin: 0.5em auto 58 | width: calc(100% + 2 * var(--content-margin)) 59 | margin-inline-start: calc(-1 * var(--content-margin)) 60 | 61 | display: block 62 | height: auto 63 | 64 | border: 1px solid var(--accent) 65 | border-left: none 66 | border-right: none 67 | 68 | hr 69 | border: 0; 70 | height: 1px; 71 | background-image: linear-gradient(to right, rgba(0, 0, 0, 0), var(--accent), rgba(0, 0, 0, 0)); 72 | 73 | .aftermath 74 | text-align: center 75 | 76 | time 77 | font-weight: 100 78 | opacity: .3 79 | 80 | nav 81 | display: inline-block 82 | font-size: 1.4em 83 | 84 | ul 85 | display: inline-block 86 | list-style-type: none 87 | margin: 0 88 | padding: 0 89 | 90 | li 91 | display: inline-block 92 | 93 | &:not(:first-child) 94 | margin-left: 14px 95 | 96 | .posts-list 97 | .post 98 | margin-top: 44px 99 | 100 | &:last-of-type 101 | hr 102 | display: none 103 | 104 | .title 105 | width: 90% 106 | margin: 0 auto 107 | font-size: 1.25em 108 | font-weight: semibold 109 | 110 | time 111 | margin-top: 32px 112 | display: block 113 | text-align: center 114 | 115 | 116 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/assets/sass/main.sass: -------------------------------------------------------------------------------- 1 | @import ../vendor/css/modern-normalize 2 | 3 | @import vars 4 | @import layout 5 | @import readability 6 | @import code 7 | 8 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/assets/sass/readability.sass: -------------------------------------------------------------------------------- 1 | body 2 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol 3 | text-rendering: optimizeLegibility 4 | text-align: start 5 | word-wrap: break-word 6 | --webkit-font-smoothing: subpixel-antialiased 7 | 8 | header 9 | a 10 | color: white 11 | text-decoration: none 12 | 13 | nav 14 | a 15 | color: var(--realbg) 16 | text-decoration: none 17 | 18 | #main 19 | font-size: 18px 20 | line-height: 29px 21 | 22 | h1 23 | font-size: 1.5em 24 | 25 | h2 26 | font-size: 1.5em 27 | 28 | h3 29 | font-size: 1.25em 30 | 31 | h4, h5, h6 32 | font-size: 1em 33 | margin: 1em 0 34 | 35 | a 36 | color: var(--accent-bright) 37 | text-decoration: none 38 | font-weight: 400 39 | 40 | &:hover 41 | text-decoration: underline 42 | text-decoration-style: dotted 43 | 44 | article 45 | h1:first-of-type 46 | margin-top: 0 47 | margin-bottom: 0.5em 48 | font-weight: bold 49 | font-size: 1.95552em 50 | line-height: 1.2141em 51 | 52 | h2:first-of-type 53 | color: var(--subhead-color) 54 | font-size: 1.46664em 55 | margin-top: -0.35em 56 | line-height: 1.27275em 57 | font-weight: normal 58 | 59 | 60 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/assets/sass/vars.sass: -------------------------------------------------------------------------------- 1 | $page-width: 800px 2 | $wider-display: 500px 3 | 4 | $accent: rgb(183, 64, 233) 5 | 6 | *:root 7 | --realbg: rgb(230, 230, 230) 8 | --bgbg: rgb(255, 255, 255) 9 | --bg: rgb(255, 255, 255) 10 | --accent: #{$accent} 11 | --accent-bright: #{$accent} 12 | --accent-light: #{lighten($accent, 20%)} 13 | --font-color: rgb(27, 27, 27) 14 | --subhead-color: rgba(27, 27, 27, 0.65) 15 | --shadow-color: rgba(0, 0, 0, 0.2) 16 | --blockquote: rgba(0, 0, 0, 0.65) 17 | --blockquote-accent: rgba(0, 0, 0, 0.1) 18 | 19 | @media (min-width: $page-width) 20 | *:root 21 | --bgbg: rgb(230, 230, 230) 22 | 23 | @media (prefers-color-scheme: dark) 24 | *:root 25 | --realbg: rgb(50, 50, 51) 26 | --bgbg: rgb(74, 74, 77) 27 | --bg: rgb(74, 74, 77) 28 | --accent: #{darken($accent, 28%)} 29 | --accent-bright: #{lighten($accent, 20%)} 30 | --accent-light: #{lighten($accent, 30%)} 31 | --font-color: rgba(255, 255, 255, 0.78) 32 | --subhead-color: rgba(255, 255, 255, 0.65) 33 | --blockquote: rgba(255, 255, 255, 0.6) 34 | --blockquote-accent: rgba(255, 255, 255, 0.15) 35 | 36 | @media (prefers-color-scheme: dark) and (min-width: $page-width) 37 | *:root 38 | --bgbg: rgb(50, 50, 51) 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/assets/vendor/css/modern-normalize.css: -------------------------------------------------------------------------------- 1 | /*! modern-normalize | MIT License | https://github.com/sindresorhus/modern-normalize */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * Use a better box model (opinionated). 8 | */ 9 | 10 | html { 11 | box-sizing: border-box; 12 | } 13 | 14 | *, 15 | *::before, 16 | *::after { 17 | box-sizing: inherit; 18 | } 19 | 20 | /** 21 | * Use a more readable tab size (opinionated). 22 | */ 23 | 24 | :root { 25 | -moz-tab-size: 4; 26 | tab-size: 4; 27 | } 28 | 29 | /** 30 | * 1. Correct the line height in all browsers. 31 | * 2. Prevent adjustments of font size after orientation changes in iOS. 32 | */ 33 | 34 | html { 35 | line-height: 1.15; /* 1 */ 36 | -webkit-text-size-adjust: 100%; /* 2 */ 37 | } 38 | 39 | /* Sections 40 | ========================================================================== */ 41 | 42 | /** 43 | * Remove the margin in all browsers. 44 | */ 45 | 46 | body { 47 | margin: 0; 48 | } 49 | 50 | /** 51 | * Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) 52 | */ 53 | 54 | body { 55 | font-family: 56 | -apple-system, 57 | BlinkMacSystemFont, 58 | 'Segoe UI', 59 | Roboto, 60 | Helvetica, 61 | Arial, 62 | sans-serif, 63 | 'Apple Color Emoji', 64 | 'Segoe UI Emoji', 65 | 'Segoe UI Symbol'; 66 | } 67 | 68 | /* Grouping content 69 | ========================================================================== */ 70 | 71 | /** 72 | * Add the correct height in Firefox. 73 | */ 74 | 75 | hr { 76 | height: 0; 77 | } 78 | 79 | /* Text-level semantics 80 | ========================================================================== */ 81 | 82 | /** 83 | * Add the correct text decoration in Chrome, Edge, and Safari. 84 | */ 85 | 86 | abbr[title] { 87 | text-decoration: underline dotted; 88 | } 89 | 90 | /** 91 | * Add the correct font weight in Chrome, Edge, and Safari. 92 | */ 93 | 94 | b, 95 | strong { 96 | font-weight: bolder; 97 | } 98 | 99 | /** 100 | * 1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) 101 | * 2. Correct the odd `em` font sizing in all browsers. 102 | */ 103 | 104 | code, 105 | kbd, 106 | samp, 107 | pre { 108 | font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in all browsers. 122 | */ 123 | 124 | sub, 125 | sup { 126 | font-size: 75%; 127 | line-height: 0; 128 | position: relative; 129 | vertical-align: baseline; 130 | } 131 | 132 | sub { 133 | bottom: -0.25em; 134 | } 135 | 136 | sup { 137 | top: -0.5em; 138 | } 139 | 140 | /* Forms 141 | ========================================================================== */ 142 | 143 | /** 144 | * 1. Change the font styles in all browsers. 145 | * 2. Remove the margin in Firefox and Safari. 146 | */ 147 | 148 | button, 149 | input, 150 | optgroup, 151 | select, 152 | textarea { 153 | font-family: inherit; /* 1 */ 154 | font-size: 100%; /* 1 */ 155 | line-height: 1.15; /* 1 */ 156 | margin: 0; /* 2 */ 157 | } 158 | 159 | /** 160 | * Remove the inheritance of text transform in Edge and Firefox. 161 | * 1. Remove the inheritance of text transform in Firefox. 162 | */ 163 | 164 | button, 165 | select { /* 1 */ 166 | text-transform: none; 167 | } 168 | 169 | /** 170 | * Correct the inability to style clickable types in iOS and Safari. 171 | */ 172 | 173 | button, 174 | [type='button'], 175 | [type='reset'], 176 | [type='submit'] { 177 | -webkit-appearance: button; 178 | } 179 | 180 | /** 181 | * Remove the inner border and padding in Firefox. 182 | */ 183 | 184 | button::-moz-focus-inner, 185 | [type='button']::-moz-focus-inner, 186 | [type='reset']::-moz-focus-inner, 187 | [type='submit']::-moz-focus-inner { 188 | border-style: none; 189 | padding: 0; 190 | } 191 | 192 | /** 193 | * Restore the focus styles unset by the previous rule. 194 | */ 195 | 196 | button:-moz-focusring, 197 | [type='button']:-moz-focusring, 198 | [type='reset']:-moz-focusring, 199 | [type='submit']:-moz-focusring { 200 | outline: 1px dotted ButtonText; 201 | } 202 | 203 | /** 204 | * Correct the padding in Firefox. 205 | */ 206 | 207 | fieldset { 208 | padding: 0.35em 0.75em 0.625em; 209 | } 210 | 211 | /** 212 | * Remove the padding so developers are not caught out when they zero out `fieldset` elements in all browsers. 213 | */ 214 | 215 | legend { 216 | padding: 0; 217 | } 218 | 219 | /** 220 | * Add the correct vertical alignment in Chrome and Firefox. 221 | */ 222 | 223 | progress { 224 | vertical-align: baseline; 225 | } 226 | 227 | /** 228 | * Correct the cursor style of increment and decrement buttons in Safari. 229 | */ 230 | 231 | [type='number']::-webkit-inner-spin-button, 232 | [type='number']::-webkit-outer-spin-button { 233 | height: auto; 234 | } 235 | 236 | /** 237 | * 1. Correct the odd appearance in Chrome and Safari. 238 | * 2. Correct the outline style in Safari. 239 | */ 240 | 241 | [type='search'] { 242 | -webkit-appearance: textfield; /* 1 */ 243 | outline-offset: -2px; /* 2 */ 244 | } 245 | 246 | /** 247 | * Remove the inner padding in Chrome and Safari on macOS. 248 | */ 249 | 250 | [type='search']::-webkit-search-decoration { 251 | -webkit-appearance: none; 252 | } 253 | 254 | /** 255 | * 1. Correct the inability to style clickable types in iOS and Safari. 256 | * 2. Change font properties to `inherit` in Safari. 257 | */ 258 | 259 | ::-webkit-file-upload-button { 260 | -webkit-appearance: button; /* 1 */ 261 | font: inherit; /* 2 */ 262 | } 263 | 264 | /* Interactive 265 | ========================================================================== */ 266 | 267 | /* 268 | * Add the correct display in Chrome and Safari. 269 | */ 270 | 271 | summary { 272 | display: list-item; 273 | } 274 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/layouts/404.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/layouts/_default/baseof.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{- partial "head.html" . -}} 4 | 5 |
6 | {{- partial "header.html" . -}} 7 |
8 | {{- block "main" . }}{{- end }} 9 |
10 | {{- partial "footer.html" . -}} 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/layouts/_default/list.html: -------------------------------------------------------------------------------- 1 | {{ define "main" }} 2 |
3 |
4 | {{- .Content -}} 5 |
6 |
7 | {{- partial "posts-list.html" . -}} 8 |
9 |
10 | {{ end }} 11 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/layouts/_default/single.html: -------------------------------------------------------------------------------- 1 | {{ define "main" }} 2 |
3 |

{{ .Title }}

4 | {{ .Content }} 5 |
6 |
7 | {{ if .Date }} 8 | 11 | {{ end }} 12 |
13 |
14 | {{ end }} 15 | 16 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/layouts/index.html: -------------------------------------------------------------------------------- 1 | {{ define "main" }} 2 |
3 |
4 | {{- .Content -}} 5 |
6 |
7 | {{ with .Site.GetPage "/posts" }} 8 | {{- partial "posts-list.html" . -}} 9 | {{ end }} 10 |
11 |
12 | {{ end }} 13 | 14 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/layouts/index.rss.xml: -------------------------------------------------------------------------------- 1 | {{ printf "" | safeHTML }} 2 | 3 | 4 | {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }} 5 | {{ .Permalink }} 6 | Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }} 7 | Hugo -- gohugo.io{{ with .Site.LanguageCode }} 8 | {{.}}{{end}}{{ with .Site.Author.email }} 9 | {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Author.email }} 10 | {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Copyright }} 11 | {{.}}{{end}}{{ if not .Date.IsZero }} 12 | {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}{{ end }} 13 | {{ with .OutputFormats.Get "RSS" }} 14 | {{ printf "" .Permalink .MediaType | safeHTML }} 15 | {{ end }} 16 | {{ range .Pages }} 17 | 18 | {{ .Title }} 19 | {{ .Permalink }} 20 | {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }} 21 | {{ with .Site.Author.email }}{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}} 22 | {{ .Permalink }} 23 | {{ .Content | html }} 24 | 25 | {{ end }} 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/layouts/partials/css.html: -------------------------------------------------------------------------------- 1 | {{ $bundleRaw := resources.Get "/sass/main.sass" | resources.ExecuteAsTemplate "/css/main.tmp.css" . }} 2 | 3 | {{ $cssOpts := (dict "targetPath" "/css/main.css" "enableSourceMap" true ) }} 4 | {{ $bundle := $bundleRaw | toCSS $cssOpts | minify | fingerprint }} 5 | 6 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/layouts/partials/footer.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/layouts/partials/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ .Title }} 5 | {{ partial "social-preview.html" . }} 6 | {{ partial "css.html" . }} 7 | {{ range .AlternativeOutputFormats -}} 8 | {{ printf `` .Rel .MediaType.Type .RelPermalink $.Site.Title | safeHTML }} 9 | {{ end -}} 10 | 11 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/layouts/partials/header.html: -------------------------------------------------------------------------------- 1 |
2 |

{{.Site.Title}}

3 |
4 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/layouts/partials/js.html: -------------------------------------------------------------------------------- 1 | {{ $index := resources.Get "/js/index.js" | minify }} 2 | {{ $scripts := slice $index | resources.Concat "/js/bundle.js" | fingerprint }} 3 | 4 | 5 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/layouts/partials/posts-list.html: -------------------------------------------------------------------------------- 1 |
2 | {{ range .Pages.ByPublishDate.Reverse }} 3 |
4 |
5 | {{ .Title }} 6 |
7 | 10 |
11 |
12 | {{ end }} 13 |
14 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/layouts/partials/social-preview.html: -------------------------------------------------------------------------------- 1 | {{ $description := .Params.Description | default "A thing @searls wrote for you." }} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/static/marketing/themes/ultralight/theme.toml: -------------------------------------------------------------------------------- 1 | # theme.toml template for a Hugo theme 2 | # See https://github.com/gohugoio/hugoThemes#themetoml for an example 3 | 4 | name = "Ultralight" 5 | license = "Unlicensed" 6 | description = "Private" 7 | homepage = "http://github.com/searls/hugo-theme-ultralight" 8 | tags = [] 9 | features = [] 10 | min_version = "0.54" 11 | 12 | [author] 13 | name = "Justin Searls" 14 | homepage = "https://twitter.com/searls" 15 | -------------------------------------------------------------------------------- /example/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/storage/.keep -------------------------------------------------------------------------------- /example/test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 4 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400] 5 | end 6 | -------------------------------------------------------------------------------- /example/test/channels/application_cable/connection_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase 4 | # test "connects with cookies" do 5 | # cookies.signed[:user_id] = 42 6 | # 7 | # connect 8 | # 9 | # assert_equal connection.user_id, "42" 10 | # end 11 | end 12 | -------------------------------------------------------------------------------- /example/test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/test/controllers/.keep -------------------------------------------------------------------------------- /example/test/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/test/fixtures/.keep -------------------------------------------------------------------------------- /example/test/fixtures/files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/test/fixtures/files/.keep -------------------------------------------------------------------------------- /example/test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/test/helpers/.keep -------------------------------------------------------------------------------- /example/test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/test/integration/.keep -------------------------------------------------------------------------------- /example/test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/test/mailers/.keep -------------------------------------------------------------------------------- /example/test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/test/models/.keep -------------------------------------------------------------------------------- /example/test/system/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/test/system/.keep -------------------------------------------------------------------------------- /example/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require_relative "../config/environment" 3 | require "rails/test_help" 4 | 5 | class ActiveSupport::TestCase 6 | # Run tests in parallel with specified workers 7 | parallelize(workers: :number_of_processors) 8 | 9 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 10 | fixtures :all 11 | 12 | # Add more helper methods to be used by all tests here... 13 | end 14 | -------------------------------------------------------------------------------- /example/tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/tmp/.keep -------------------------------------------------------------------------------- /example/vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/static-rails/2d1a576370a634519b24a2175548abda0c71c58b/example/vendor/.keep -------------------------------------------------------------------------------- /lib/generators/static_rails/initializer_generator.rb: -------------------------------------------------------------------------------- 1 | module StaticRails 2 | module Generators 3 | class InitializerGenerator < ::Rails::Generators::Base 4 | source_root File.expand_path("../../templates", __FILE__) 5 | 6 | desc "Creates a sample static-rails initializer." 7 | def copy_initializer 8 | copy_file "static.rb", "config/initializers/static.rb" 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/generators/templates/static.rb: -------------------------------------------------------------------------------- 1 | StaticRails.config do |config| 2 | # Control whether static-rails adds a middleware to proxy requests to your static site servers 3 | # config.proxy_requests = !Rails.env.production? 4 | 5 | # Control whether static-rails adds a middleware to serve your sites' compiled static assets with Static::Rack (has no effect if proxy_requests is enabled) 6 | # config.serve_compiled_assets = Rails.env.production? 7 | 8 | # Timeout in seconds to wait when proxying to a static server 9 | # (Applies when a site has both start_server and ping_server set to true) 10 | # config.ping_server_timeout = 5 11 | 12 | # When true, both the proxy & static asset middleware will set a cookie 13 | # named "_csrf_token" to the Rails CSRF token, allowing any client-side 14 | # API requests to take advantage of Rails' request forgery protection 15 | # config.set_csrf_token_cookie = false 16 | 17 | # The list of static sites you are hosting with static-rails. 18 | # Note that order matters! Request will be forwarded to the first site that 19 | # matches the subdomain and root path (this probably means you want any sites 20 | # with subdomains listed first) 21 | config.sites = [ 22 | # { 23 | # # Unique name for the site 24 | # name: "blog", 25 | # 26 | # # File path to the static app relative to Rails root path 27 | # source_dir: "static/blog", 28 | # 29 | # # Constrain static app to the following subdomain (omit or leave nil 30 | # # if you aren't hosting the site on a subdomain) 31 | # url_subdomain: "blog", 32 | # 33 | # # Mount the static site web hosting to a certain sub-path (e.g. "/docs") 34 | # url_root_path: "/", 35 | # 36 | # # Don't serve/redirect routes whose paths start with these strings 37 | # url_skip_paths_starting_with: ["/api"], 38 | # 39 | # # Whether to run the local development/test server or not 40 | # start_server: !Rails.env.production?, 41 | # 42 | # # If start_server is true, wait to proxy requests to the server until it 43 | # # can connect to server_host over TCP on server_port 44 | # ping_server: true, 45 | # 46 | # # Any environment variables you need to pass to the server & compile 47 | # # commands as a hash (e.g. `env: {"BUNDLE_PATH" => "vendor/bundle"}`) 48 | # env: {}, 49 | # 50 | # # The command to execute when running the static app in local 51 | # # development/test environments 52 | # server_command: "hugo server", 53 | # 54 | # # The host the local development/test server should be reached on 55 | # server_host: "localhost", 56 | # 57 | # # The port the local development/test server should be reached on 58 | # server_port: "1313", 59 | # 60 | # # The root path on the local development/test server to which requests 61 | # # should be forwarded 62 | # server_path: "/", 63 | # 64 | # # The command to execute when building the static app for production 65 | # compile_command: "hugo", 66 | # 67 | # # The destination of production-compiled assets, relative to Rails root 68 | # compile_dir: "static/blog/dist", 69 | # 70 | # # A 404 page to be sent when serving compiled assets and no file matches 71 | # compile_404_file_path: "404.html" 72 | # }, 73 | ] 74 | end 75 | -------------------------------------------------------------------------------- /lib/static-rails.rb: -------------------------------------------------------------------------------- 1 | module StaticRails 2 | end 3 | 4 | require_relative "static-rails/error" 5 | require_relative "static-rails/version" 6 | require_relative "static-rails/configuration" 7 | require_relative "static-rails/compile" 8 | require_relative "static-rails/railtie" 9 | -------------------------------------------------------------------------------- /lib/static-rails/compile.rb: -------------------------------------------------------------------------------- 1 | module StaticRails 2 | def self.compile 3 | CompilesSites.new.call(StaticRails.config) 4 | end 5 | 6 | class CompilesSites 7 | def call(config) 8 | config.sites.each do |site| 9 | Dir.chdir(config.app.root.join(site.source_dir)) do 10 | Bundler.with_unbundled_env do 11 | puts "=> Compiling static site \"#{site.name}\" to #{site.compile_dir}" 12 | result = system(site.env, site.compile_command) 13 | unless result == true 14 | raise Error.new("Compilation of static site \"#{site.name}\" failed (in directory \"#{site.source_dir}\" with command: `#{site.compile_command}`)") 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/static-rails/configuration.rb: -------------------------------------------------------------------------------- 1 | require_relative "site" 2 | 3 | module StaticRails 4 | def self.config(&blk) 5 | @configuration ||= Configuration.new 6 | 7 | @configuration.tap do |config| 8 | blk&.call(config) 9 | end 10 | end 11 | 12 | class Configuration 13 | # When Rails invokes our Railtie, we'll save off a reference to the Rails app 14 | attr_accessor :app 15 | 16 | # When true, our middleware will proxy requests to static site servers 17 | attr_accessor :proxy_requests 18 | 19 | # When true, our middleware will serve sites' compiled asset files 20 | attr_accessor :serve_compiled_assets 21 | 22 | # Number of seconds to wait on sites to confirm servers are ready 23 | attr_accessor :ping_server_timeout 24 | 25 | # When true, a cookie named "_csrf_token" will be set by static-rails middleware 26 | attr_accessor :set_csrf_token_cookie 27 | 28 | def initialize 29 | @sites = [] 30 | @proxy_requests = !Rails.env.production? 31 | @serve_compiled_assets = Rails.env.production? 32 | @ping_server_timeout = 5 33 | @set_csrf_token_cookie = false 34 | end 35 | 36 | attr_reader :sites 37 | def sites=(sites) 38 | @sites = Array.wrap(sites).map { |site| 39 | Site.new(**site) 40 | } 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/static-rails/determines_whether_to_handle_request.rb: -------------------------------------------------------------------------------- 1 | module StaticRails 2 | class DeterminesWhetherToHandleRequest 3 | def initialize 4 | @matches_request_to_static_site = MatchesRequestToStaticSite.new 5 | end 6 | 7 | def call(env) 8 | req = Rack::Request.new(env) 9 | 10 | (req.get? || req.head?) && 11 | (StaticRails.config.proxy_requests || StaticRails.config.serve_compiled_assets) && 12 | @matches_request_to_static_site.call(req) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/static-rails/error.rb: -------------------------------------------------------------------------------- 1 | module StaticRails 2 | class Error < StandardError; end 3 | end 4 | -------------------------------------------------------------------------------- /lib/static-rails/file_handler.rb: -------------------------------------------------------------------------------- 1 | module StaticRails 2 | # This class was extracted from Ruby on Rails: 3 | # 4 | # - actionpack/lib/action_dispatch/middleware/static.rb 5 | # 6 | # Copyright (c) 2005-2020 David Heinemeier Hansson, Ryan Edward Hall, Jeremy Daer 7 | # 8 | # License here: https://github.com/rails/rails/blob/master/MIT-LICENSE 9 | # 10 | # This endpoint serves static files from disk using Rack::File. 11 | # 12 | # URL paths are matched with static files according to expected 13 | # conventions: +path+, +path+.html, +path+/index.html. 14 | # 15 | # Precompressed versions of these files are checked first. Brotli (.br) 16 | # and gzip (.gz) files are supported. If +path+.br exists, this 17 | # endpoint returns that file with a +Content-Encoding: br+ header. 18 | # 19 | # If no matching file is found, this endpoint responds 404 Not Found. 20 | # 21 | # Pass the +root+ directory to search for matching files, an optional 22 | # +index: "index"+ to change the default +path+/index.html, and optional 23 | # additional response headers. 24 | class FileHandler 25 | # Accept-Encoding value -> file extension 26 | PRECOMPRESSED = { 27 | "br" => ".br", 28 | "gzip" => ".gz", 29 | "identity" => nil 30 | } 31 | 32 | def initialize(root, index: "index", headers: {}, precompressed: %i[br gzip], compressible_content_types: /\A(?:text\/|application\/javascript)/) 33 | @root = root.chomp("/").b 34 | @index = index 35 | 36 | @precompressed = Array(precompressed).map(&:to_s) | %w[identity] 37 | @compressible_content_types = compressible_content_types 38 | 39 | @file_server = ::Rack::File.new(@root, headers) 40 | end 41 | 42 | def call(env) 43 | attempt(env) || @file_server.call(env) 44 | end 45 | 46 | def attempt(env) 47 | request = Rack::Request.new env 48 | 49 | if request.get? || request.head? 50 | if (found = find_file(request.path_info, accept_encoding: request.accept_encoding)) 51 | serve request, *found 52 | end 53 | end 54 | end 55 | 56 | def serve(request, filepath, content_headers) 57 | original, request.path_info = request.path_info, ::Rack::Utils.escape_path(filepath).b 58 | 59 | @file_server.call(request.env).tap do |status, headers, body| 60 | # Omit Content-Encoding/Type/etc headers for 304 Not Modified 61 | if status != 304 62 | headers.update(content_headers) 63 | end 64 | end 65 | ensure 66 | request.path_info = original 67 | end 68 | 69 | # Match a URI path to a static file to be served. 70 | # 71 | # Used by the +Static+ class to negotiate a servable file in the 72 | # +public/+ directory (see Static#call). 73 | # 74 | # Checks for +path+, +path+.html, and +path+/index.html files, 75 | # in that order, including .br and .gzip compressed extensions. 76 | # 77 | # If a matching file is found, the path and necessary response headers 78 | # (Content-Type, Content-Encoding) are returned. 79 | def find_file(path_info, accept_encoding:) 80 | each_candidate_filepath(path_info) do |filepath, content_type| 81 | if (response = try_files(filepath, content_type, accept_encoding: accept_encoding)) 82 | return response 83 | end 84 | end 85 | end 86 | 87 | private 88 | 89 | def try_files(filepath, content_type, accept_encoding:) 90 | headers = {"Content-Type" => content_type} 91 | 92 | if compressible? content_type 93 | try_precompressed_files filepath, headers, accept_encoding: accept_encoding 94 | elsif file_readable? filepath 95 | [filepath, headers] 96 | end 97 | end 98 | 99 | def try_precompressed_files(filepath, headers, accept_encoding:) 100 | each_precompressed_filepath(filepath) do |content_encoding, precompressed_filepath| 101 | if file_readable? precompressed_filepath 102 | # Identity encoding is default, so we skip Accept-Encoding 103 | # negotiation and needn't set Content-Encoding. 104 | # 105 | # Vary header is expected when we've found other available 106 | # encodings that Accept-Encoding ruled out. 107 | if content_encoding == "identity" 108 | return precompressed_filepath, headers 109 | else 110 | headers["Vary"] = "Accept-Encoding" 111 | 112 | if accept_encoding.any? { |enc, _| /\b#{content_encoding}\b/i.match?(enc) } 113 | headers["Content-Encoding"] = content_encoding 114 | return precompressed_filepath, headers 115 | end 116 | end 117 | end 118 | end 119 | end 120 | 121 | def file_readable?(path) 122 | file_stat = File.stat(File.join(@root, path.b)) 123 | rescue SystemCallError 124 | false 125 | else 126 | file_stat.file? && file_stat.readable? 127 | end 128 | 129 | def compressible?(content_type) 130 | @compressible_content_types.match?(content_type) 131 | end 132 | 133 | def each_precompressed_filepath(filepath) 134 | @precompressed.each do |content_encoding| 135 | precompressed_ext = PRECOMPRESSED.fetch(content_encoding) 136 | yield content_encoding, "#{filepath}#{precompressed_ext}" 137 | end 138 | 139 | nil 140 | end 141 | 142 | def each_candidate_filepath(path_info) 143 | return unless (path = clean_path(path_info)) 144 | 145 | ext = ::File.extname(path) 146 | content_type = ::Rack::Mime.mime_type(ext, nil) 147 | yield path, content_type || "text/plain" 148 | 149 | # Tack on .html and /index.html only for paths that don't have 150 | # an explicit, resolvable file extension. No need to check 151 | # for foo.js.html and foo.js/index.html. 152 | unless content_type 153 | default_ext = ::ActionController::Base.default_static_extension 154 | if ext != default_ext 155 | default_content_type = ::Rack::Mime.mime_type(default_ext, "text/plain") 156 | 157 | yield "#{path}#{default_ext}", default_content_type 158 | yield "#{path}/#{@index}#{default_ext}", default_content_type 159 | end 160 | end 161 | 162 | nil 163 | end 164 | 165 | def clean_path(path_info) 166 | path = ::Rack::Utils.unescape_path path_info.chomp("/") 167 | if ::Rack::Utils.valid_path? path 168 | ::Rack::Utils.clean_path_info path 169 | end 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /lib/static-rails/gets_csrf_token.rb: -------------------------------------------------------------------------------- 1 | require_relative "request_forgery_protection_fallback" 2 | 3 | module StaticRails 4 | class GetsCsrfToken 5 | include RequestForgeryProtectionFallback 6 | 7 | def call(req) 8 | masked_authenticity_token(req.session) 9 | end 10 | 11 | private 12 | 13 | [ 14 | :csrf_token_hmac, 15 | :mask_token, 16 | :xor_byte_strings 17 | ].each do |method| 18 | define_method method do |*args, **kwargs, &blk| 19 | ActionController::RequestForgeryProtection.instance_method(method).bind(self).call(*args, **kwargs, &blk) 20 | end 21 | end 22 | 23 | def masked_authenticity_token(session, form_options: {}) 24 | ActionController::RequestForgeryProtection.instance_method(:masked_authenticity_token).bind(self).call(session, form_options: form_options) 25 | end 26 | 27 | def global_csrf_token(session) 28 | ActionController::RequestForgeryProtection.instance_method(:global_csrf_token).bind(self).call(session) 29 | end 30 | 31 | def real_csrf_token(session) 32 | ActionController::RequestForgeryProtection.instance_method(:real_csrf_token).bind(self).call(session) 33 | end 34 | 35 | def per_form_csrf_tokens 36 | false 37 | end 38 | 39 | def urlsafe_csrf_tokens 40 | Rails.application.config.action_controller.urlsafe_csrf_tokens 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/static-rails/matches_request_to_static_site.rb: -------------------------------------------------------------------------------- 1 | module StaticRails 2 | class MatchesRequestToStaticSite 3 | def call(request) 4 | StaticRails.config.sites.find { |site| 5 | subdomain_match?(site, request) && path_match?(site, request) && !skip_path?(site, request) 6 | } 7 | end 8 | 9 | private 10 | 11 | def subdomain_match?(site, request) 12 | return true if site.url_subdomain.nil? 13 | 14 | expected = site.url_subdomain.split(".") 15 | actual = request.host.split(".") 16 | 17 | expected.enum_for.with_index.all? { |sub, i| actual[i] == sub } 18 | end 19 | 20 | def path_match?(site, request) 21 | request.path_info.start_with?(site.url_root_path) 22 | end 23 | 24 | def skip_path?(site, request) 25 | site.url_skip_paths_starting_with.any? { |path_start| 26 | request.path_info.start_with?(path_start) 27 | } 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/static-rails/proxy_middleware.rb: -------------------------------------------------------------------------------- 1 | require "rack-proxy" 2 | 3 | require_relative "matches_request_to_static_site" 4 | require_relative "server_store" 5 | 6 | module StaticRails 7 | class ProxyMiddleware < Rack::Proxy 8 | def initialize(app) 9 | @matches_request_to_static_site = MatchesRequestToStaticSite.new 10 | @app = app 11 | @servers = {} 12 | super 13 | end 14 | 15 | def perform_request(env) 16 | return @app.call(env) unless StaticRails.config.proxy_requests 17 | 18 | server_store = ServerStore.instance 19 | server_store.ensure_all_servers_are_started 20 | 21 | req = Rack::Request.new(env) 22 | if (req.get? || req.head?) && (site = @matches_request_to_static_site.call(req)) 23 | if site.ping_server && (server = server_store.server_for(site)) 24 | server.wait_until_ready 25 | end 26 | 27 | @backend = URI("http://#{site.server_host}:#{site.server_port}") 28 | env["HTTP_HOST"] = @backend.host 29 | env["PATH_INFO"] = forwarding_path(site, req) 30 | 31 | super(env) 32 | else 33 | @app.call(env) 34 | end 35 | end 36 | 37 | private 38 | 39 | def forwarding_path(site, req) 40 | req_path = req.path_info 41 | 42 | if req_path == site.url_root_path && !req_path.end_with?("/") 43 | req_path + "/" # <- Necessary for getting jekyll, possibly hugo to serve the root 44 | else 45 | req_path 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/static-rails/rack_server_check.rb: -------------------------------------------------------------------------------- 1 | module StaticRails 2 | # Shamelessly ripped out of @danmeyer's Coverband: https://github.com/danmayer/coverband/blob/master/lib/coverband/integrations/rack_server_check.rb#L14 3 | # 4 | # Copyright (c) 2010-2018 Dan Mayer 5 | # 6 | # Distributed under the MIT License 7 | # 8 | # Details: 9 | # https://github.com/danmayer/coverband/blob/master/LICENSE.txt 10 | class RackServerCheck 11 | def self.running? 12 | new(Kernel.caller_locations).running? 13 | end 14 | 15 | def initialize(stack) 16 | @stack = stack 17 | end 18 | 19 | def running? 20 | rack_server? || rails_server? 21 | end 22 | 23 | def rack_server? 24 | @stack.any? { |line| line.path.include?("lib/rack/") } 25 | end 26 | 27 | def rails_server? 28 | @stack.any? do |location| 29 | ( 30 | (location.path.include?("rails/commands/commands_tasks.rb") && location.label == "server") || 31 | (location.path.include?("rails/commands/server/server_command.rb") && location.label == "perform") 32 | ) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/static-rails/railtie.rb: -------------------------------------------------------------------------------- 1 | require_relative "rack_server_check" 2 | require_relative "server_store" 3 | require_relative "site_middleware" 4 | require_relative "site_plus_csrf_middleware" 5 | 6 | module StaticRails 7 | class Railtie < ::Rails::Railtie 8 | rake_tasks do 9 | load "tasks/static-rails.rake" 10 | end 11 | 12 | initializer "static_rails.middleware" do 13 | config.app_middleware.insert_after Rack::Sendfile, SiteMiddleware 14 | config.app_middleware.use SitePlusCsrfMiddleware 15 | end 16 | 17 | config.after_initialize do |app| 18 | static_rails_config = StaticRails.config 19 | static_rails_config.app = app 20 | 21 | if RackServerCheck.running? 22 | ServerStore.instance.ensure_all_servers_are_started 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/static-rails/request_forgery_protection_fallback.rb: -------------------------------------------------------------------------------- 1 | module StaticRails 2 | module RequestForgeryProtectionFallback 3 | def method_missing(method_name, *args, **kwargs, &blk) 4 | if respond_to?(method_name) 5 | ActionController::RequestForgeryProtection.instance_method(method_name).bind(self).call(*args, **kwargs, &blk) 6 | else 7 | super 8 | end 9 | end 10 | 11 | def respond_to?(method_name, *args) 12 | ActionController::RequestForgeryProtection.instance_method(method_name) || super 13 | end 14 | 15 | def respond_to_missing?(method_name, *args) 16 | ActionController::RequestForgeryProtection.instance_method(method_name) || super 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/static-rails/server.rb: -------------------------------------------------------------------------------- 1 | require_relative "waits_for_connection" 2 | 3 | module StaticRails 4 | class Server 5 | def initialize(site) 6 | @site = site 7 | @ready = false 8 | @waits_for_connection = WaitsForConnection.new 9 | end 10 | 11 | def start 12 | return if started? 13 | @pid = spawn_process 14 | set_at_exit_hook 15 | nil 16 | end 17 | 18 | def started? 19 | return false unless @pid.present? 20 | 21 | begin 22 | Process.getpgid(@pid) 23 | true 24 | rescue Errno::ESRCH 25 | @ready = false 26 | false 27 | end 28 | end 29 | 30 | def wait_until_ready 31 | return if @ready 32 | @waits_for_connection.call(@site) 33 | @ready = true 34 | end 35 | 36 | private 37 | 38 | def spawn_process 39 | options = { 40 | in: "/dev/null", 41 | out: "/dev/stdout", 42 | err: "/dev/stderr", 43 | close_others: true, 44 | chdir: StaticRails.config.app.root.join(@site.source_dir).to_s 45 | } 46 | 47 | Rails.logger.info "=> Starting #{@site.name} static server" 48 | Bundler.with_unbundled_env do 49 | Process.spawn(@site.env, @site.server_command, options).tap do |pid| 50 | Process.detach(pid) 51 | end 52 | end 53 | end 54 | 55 | def set_at_exit_hook 56 | return if @at_exit_hook_set 57 | at_exit do 58 | if started? 59 | Rails.logger.info "=> Stopping #{@site.name} static server" 60 | Process.kill("INT", @pid) 61 | end 62 | end 63 | @at_exit_hook_set = true 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/static-rails/server_store.rb: -------------------------------------------------------------------------------- 1 | require_relative "server" 2 | 3 | module StaticRails 4 | class ServerStore 5 | def self.instance 6 | @instance ||= new 7 | end 8 | 9 | def ensure_all_servers_are_started 10 | StaticRails.config.sites.select(&:start_server).each do |site| 11 | server_for(site).start 12 | end 13 | end 14 | 15 | def server_for(site) 16 | @servers[site] ||= Server.new(site) 17 | end 18 | 19 | private 20 | 21 | def initialize 22 | @servers = {} 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/static-rails/site.rb: -------------------------------------------------------------------------------- 1 | module StaticRails 2 | class Site < Struct.new( 3 | :name, 4 | :url_subdomain, 5 | :url_root_path, 6 | :url_skip_paths_starting_with, 7 | :source_dir, 8 | :start_server, 9 | :ping_server, 10 | :env, 11 | :server_command, 12 | :server_host, 13 | :server_port, 14 | :server_path, 15 | :compile_command, 16 | :compile_dir, 17 | :compile_404_file_path, 18 | keyword_init: true 19 | ) 20 | 21 | def initialize( 22 | url_root_path: "/", 23 | url_skip_paths_starting_with: [], 24 | start_server: !Rails.env.production?, 25 | ping_server: true, 26 | env: {}, 27 | server_host: "localhost", 28 | server_path: "/", 29 | **other_kwargs 30 | ) 31 | @url_root_path = url_root_path 32 | @url_skip_paths_starting_with = url_skip_paths_starting_with 33 | @start_server = start_server 34 | @ping_server = ping_server 35 | @env = env 36 | @server_host = server_host 37 | @server_path = server_path 38 | super 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/static-rails/site_middleware.rb: -------------------------------------------------------------------------------- 1 | require_relative "proxy_middleware" 2 | require_relative "static_middleware" 3 | require_relative "determines_whether_to_handle_request" 4 | 5 | module StaticRails 6 | class SiteMiddleware 7 | PATH_INFO_OBFUSCATION = "__static-rails__" 8 | 9 | def initialize(app) 10 | @app = app 11 | @proxy_middleware = ProxyMiddleware.new(app) 12 | @static_middleware = StaticMiddleware.new(app) 13 | @determines_whether_to_handle_request = DeterminesWhetherToHandleRequest.new 14 | end 15 | 16 | def call(env) 17 | return @app.call(env) unless @determines_whether_to_handle_request.call(env) 18 | 19 | if require_csrf_before_processing_request? 20 | # You might be asking yourself what the hell is going on here. In short, 21 | # This middleware sits at the top of the stack, which is too early to 22 | # set a CSRF token in a cookie. Therefore, we've placed a subclass of 23 | # this middleware named SitePlusCsrfMiddleware near the bottom of the 24 | # middleware stack, which is slower but comes after Session::CookieStore 25 | # and therefore can write _csrf_token to the cookie. As a result, the 26 | # observable behavior to the user is identical, but the first request 27 | # to set the cookie will be marginally slower because it needs to go 28 | # deeper down the Rails middleware stack 29 | # 30 | # But! Between these two is ActionDispatch::Static. In the odd case that 31 | # a path that this middleware would serve happens to match the name of 32 | # a path in public/, kicking down the middleware stack would result in 33 | # that file being served instead of our deeper middleware being called. 34 | # So to work around this we're just making the PATH_INFO property so 35 | # ugly that there's no chance it'll match anything. When our subclass 36 | # gets its shot at this request, it'll know to remove the path 37 | # obfuscation from PATH_INFO and go about its business. 38 | # 39 | # See, easy! 40 | # 41 | # (By the way, this was all Matthew Draper's bright idea. You can 42 | # compliment him here: https://github.com/matthewd ) 43 | @app.call(env.merge("PATH_INFO" => "/" + PATH_INFO_OBFUSCATION + env["PATH_INFO"])) 44 | elsif StaticRails.config.proxy_requests 45 | @proxy_middleware.call(env) 46 | elsif StaticRails.config.serve_compiled_assets 47 | @static_middleware.call(env) 48 | end 49 | end 50 | 51 | protected 52 | 53 | # Override this in subclass since it'll call super(env) and deal itself 54 | def require_csrf_before_processing_request? 55 | StaticRails.config.set_csrf_token_cookie 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/static-rails/site_plus_csrf_middleware.rb: -------------------------------------------------------------------------------- 1 | require_relative "site_middleware" 2 | require_relative "determines_whether_to_handle_request" 3 | require_relative "validates_csrf_token" 4 | require_relative "gets_csrf_token" 5 | 6 | module StaticRails 7 | class SitePlusCsrfMiddleware < SiteMiddleware 8 | def initialize(app) 9 | @determines_whether_to_handle_request = DeterminesWhetherToHandleRequest.new 10 | @validates_csrf_token = ValidatesCsrfToken.new 11 | @gets_csrf_token = GetsCsrfToken.new 12 | super 13 | end 14 | 15 | def call(env) 16 | return @app.call(env) unless env["PATH_INFO"]&.start_with?(/\/?#{PATH_INFO_OBFUSCATION}/o) || @determines_whether_to_handle_request.call(env) 17 | 18 | env = env.merge( 19 | "PATH_INFO" => env["PATH_INFO"].gsub(/^\/?#{PATH_INFO_OBFUSCATION}/o, "") 20 | ) 21 | status, headers, body = super(env) 22 | 23 | if StaticRails.config.set_csrf_token_cookie 24 | req = Rack::Request.new(env) 25 | res = Rack::Response.new(body, status, headers) 26 | if needs_new_csrf_token?(req) 27 | res.set_cookie("_csrf_token", { 28 | value: @gets_csrf_token.call(req), 29 | path: "/" 30 | }) 31 | end 32 | res.finish 33 | else 34 | [status, headers, body] 35 | end 36 | end 37 | 38 | protected 39 | 40 | def require_csrf_before_processing_request? 41 | false 42 | end 43 | 44 | private 45 | 46 | def needs_new_csrf_token?(req) 47 | !req.cookies.has_key?("_csrf_token") || !@validates_csrf_token.call(req) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/static-rails/static_middleware.rb: -------------------------------------------------------------------------------- 1 | require "rack-proxy" 2 | 3 | require_relative "file_handler" 4 | require_relative "matches_request_to_static_site" 5 | 6 | module StaticRails 7 | class StaticMiddleware 8 | def initialize(app) 9 | @matches_request_to_static_site = MatchesRequestToStaticSite.new 10 | @app = app 11 | @file_handlers = {} 12 | end 13 | 14 | def call(env) 15 | return @app.call(env) unless StaticRails.config.serve_compiled_assets 16 | req = Rack::Request.new env 17 | 18 | if (req.get? || req.head?) && (site = @matches_request_to_static_site.call(req)) 19 | file_handler = file_handler_for(site) 20 | path = req.path_info.gsub(/^#{site.url_root_path}/, "").chomp("/") 21 | if (result = serve_file_for(file_handler, site, path, req)) 22 | return result 23 | end 24 | end 25 | 26 | @app.call(req.env) 27 | end 28 | 29 | private 30 | 31 | # The same file handler used by Rails when serving up files from /public 32 | # See: actionpack/lib/action_dispatch/middleware/static.rb 33 | def file_handler_for(site) 34 | @file_handlers[site] ||= FileHandler.new( 35 | StaticRails.config.app.root.join(site.compile_dir).to_s, 36 | headers: { 37 | "cache-control" => "public; max-age=31536000" 38 | }, 39 | compressible_content_types: /^text\/|[\/+](javascript|json|text|xml|css|yaml)$/i 40 | ) 41 | end 42 | 43 | def serve_file_for(file_handler, site, path, req) 44 | if (found = file_handler.find_file(path, accept_encoding: req.accept_encoding)) 45 | serve_file(file_handler, found, req) 46 | elsif site.compile_404_file_path.present? 47 | found = file_handler.find_file(site.compile_404_file_path, accept_encoding: req.accept_encoding) 48 | serve_file(file_handler, found, req, 404) 49 | end 50 | end 51 | 52 | def serve_file(file_handler, file, req, status_override = nil) 53 | return unless file 54 | file_handler.serve(req, *file).tap do |result| 55 | result[0] = status_override unless status_override.nil? 56 | override_cache_control_if_we_probably_shouldnt_cache!(file, result[1]) 57 | end 58 | end 59 | 60 | PROBABLY_SHOULDNT_CACHE_MIME_TYPES = [ 61 | "text/html", "application/xml", "application/rss+xml" 62 | ] 63 | def override_cache_control_if_we_probably_shouldnt_cache!(file, headers = {}) 64 | mime_type = file[1]["Content-Type"] 65 | if PROBABLY_SHOULDNT_CACHE_MIME_TYPES.include?(mime_type) 66 | headers["cache-control"] = "no-cache, no-store" 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/static-rails/validates_csrf_token.rb: -------------------------------------------------------------------------------- 1 | require_relative "request_forgery_protection_fallback" 2 | 3 | module StaticRails 4 | class ValidatesCsrfToken 5 | include RequestForgeryProtectionFallback 6 | 7 | def call(req) 8 | valid_authenticity_token?(req.session, req.cookies["_csrf_token"]) 9 | end 10 | 11 | private 12 | 13 | [ 14 | :compare_with_global_token, 15 | :global_csrf_token, 16 | :csrf_token_hmac, 17 | :valid_authenticity_token?, 18 | :unmask_token, 19 | :compare_with_real_token, 20 | :valid_per_form_csrf_token?, 21 | :xor_byte_strings, 22 | :real_csrf_token 23 | ].each do |method| 24 | define_method method do |*args, **kwargs, &blk| 25 | ActionController::RequestForgeryProtection.instance_method(method).bind(self).call(*args, **kwargs, &blk) 26 | end 27 | end 28 | 29 | def per_form_csrf_tokens 30 | false 31 | end 32 | 33 | def urlsafe_csrf_tokens 34 | Rails.application.config.action_controller.urlsafe_csrf_tokens 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/static-rails/version.rb: -------------------------------------------------------------------------------- 1 | module StaticRails 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /lib/static-rails/waits_for_connection.rb: -------------------------------------------------------------------------------- 1 | module StaticRails 2 | class WaitsForConnection 3 | def call(site) 4 | timeout = StaticRails.config.ping_server_timeout 5 | start = Time.new 6 | wait_message_logged = false 7 | 8 | loop do 9 | Socket.tcp(site.server_host, site.server_port, connect_timeout: 5) 10 | break 11 | rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL 12 | elapsed = Time.new - start 13 | if elapsed > timeout 14 | raise Error.new("Static site server \"#{site.name}\" failed to start within #{timeout} seconds. You can change the timeout with `StaticRails.config.ping_server_timeout = 42`") 15 | else 16 | unless wait_message_logged 17 | Rails.logger.info "=> Static site server \"#{site.name}\" is not yet accepting connections on #{site.server_host}:#{site.server_port}. Will try to connect for #{timeout} more seconds" 18 | wait_message_logged = true 19 | end 20 | sleep 0.3 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/tasks/static-rails.rake: -------------------------------------------------------------------------------- 1 | $stdout.sync = true 2 | 3 | def enhance_assets_precompile 4 | Rake::Task["assets:precompile"].enhance([]) do 5 | Rake::Task["static:compile"].invoke 6 | end 7 | end 8 | 9 | namespace :static do 10 | desc "Compile static sites configured with static-rails" 11 | task compile: :environment do 12 | StaticRails.compile 13 | end 14 | end 15 | 16 | if Rake::Task.task_defined?("assets:precompile") 17 | enhance_assets_precompile 18 | else 19 | Rake::Task.define_task("assets:precompile" => "static:compile") 20 | end 21 | -------------------------------------------------------------------------------- /script/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | echo "--> Installing static-rails' Ruby deps" 6 | bundle install --path vendor/bundle 7 | 8 | echo "--> Installing example app's Ruby deps" 9 | cd example 10 | bundle install --path vendor/bundle 11 | cd .. 12 | 13 | echo "--> Installing example app's JS deps" 14 | cd example 15 | yarn install 16 | cd .. 17 | 18 | echo "--> Installing example app's Jekyll site's Ruby deps" 19 | cd example/static/docs 20 | bundle install --path vendor/bundle 21 | cd ../../.. 22 | 23 | echo "--> Installing example app's Eleventy site's JS deps" 24 | cd example/static/blog-docs 25 | npm install 26 | cd ../../.. 27 | 28 | 29 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | echo "--> Running example app's tests" 6 | cd example 7 | ./script/test 8 | cd .. 9 | 10 | echo "--> Running standard:fix" 11 | bundle exec rake standard:fix 12 | 13 | -------------------------------------------------------------------------------- /static-rails.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/static-rails/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "static-rails" 5 | spec.version = StaticRails::VERSION 6 | spec.authors = ["Justin Searls"] 7 | spec.email = ["searls@gmail.com"] 8 | 9 | spec.summary = "Build & serve static sites (e.g. Jekyll, Hugo) from your Rails app" 10 | spec.homepage = "https://github.com/testdouble/static-rails" 11 | spec.license = "MIT" 12 | spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0") 13 | 14 | spec.metadata["homepage_uri"] = spec.homepage 15 | 16 | spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do 17 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|example)/}) } 18 | end 19 | spec.bindir = "exe" 20 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 21 | spec.require_paths = ["lib"] 22 | 23 | spec.add_dependency "railties", ">= 5.0.0" 24 | spec.add_dependency "rack-proxy", "~> 0.6" 25 | end 26 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 2 | require "rails/static/site" 3 | 4 | require "minitest/autorun" 5 | --------------------------------------------------------------------------------