├── .gitignore ├── app ├── javascript │ ├── utils │ │ ├── helpers.js │ │ └── api.js │ └── .DS_Store ├── assets │ ├── javascripts │ │ ├── application.js │ │ └── active_admin.js │ └── stylesheets │ │ └── active_admin.scss ├── .DS_Store ├── controllers │ ├── api │ │ ├── api_controller.rb │ │ └── home_controller.rb │ ├── errors_controller.rb │ └── home_controller.rb ├── jobs │ └── http_post_job.rb ├── views │ ├── errors │ │ ├── 404.html.erb │ │ ├── 422.html.erb │ │ └── 500.html.erb │ └── home │ │ └── index.html.erb ├── template.rb └── lib │ └── http_helper.rb ├── lib ├── generators │ └── stimulus │ │ ├── USAGE │ │ ├── templates │ │ └── controller.js.erb.tt │ │ └── stimulus_generator.rb ├── template.rb └── tasks │ └── deploy.rake.tt ├── ruby-version.tt ├── public └── robots.txt ├── dockerignore ├── spec ├── factories.rb ├── template.rb ├── support │ └── factory_bot.rb └── rails_helper.rb ├── config ├── sidekiq.yml ├── template.rb ├── initializers │ └── sidekiq.rb ├── routes.rb ├── environments │ ├── production.rb │ └── development.rb └── application.rb ├── .DS_Store ├── Procfile ├── k8s ├── sidekiq_quite.sh ├── service.yaml.tt ├── template.rb ├── cluster │ ├── lets_encrypt_issuer.yaml.tt │ └── load_balancer.yaml.tt ├── ingress.yaml.tt ├── migration.yaml.tt ├── redis.yaml.tt ├── sidekiq.yaml.tt ├── project │ └── application-nginx-conf.yaml.tt ├── web.yaml.tt ├── demo.yaml.tt └── README.md.tt ├── README.md.tt ├── gitignore ├── Dockerfile.tt ├── LICENSE ├── README.md ├── .circleci └── config.yml.tt └── template.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /app/javascript/utils/helpers.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/generators/stimulus/USAGE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ruby-version.tt: -------------------------------------------------------------------------------- 1 | <%= RUBY_VERSION %> -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / -------------------------------------------------------------------------------- /dockerignore: -------------------------------------------------------------------------------- 1 | /log 2 | /public/assets 3 | /public/packs -------------------------------------------------------------------------------- /spec/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | end 3 | -------------------------------------------------------------------------------- /config/sidekiq.yml: -------------------------------------------------------------------------------- 1 | :queues: 2 | - default 3 | - [mailers, 2] -------------------------------------------------------------------------------- /app/assets/javascripts/active_admin.js: -------------------------------------------------------------------------------- 1 | //= require arctic_admin/base 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrocket/rails-template/HEAD/.DS_Store -------------------------------------------------------------------------------- /app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrocket/rails-template/HEAD/app/.DS_Store -------------------------------------------------------------------------------- /app/controllers/api/api_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::ApiController < ActionController::API 2 | end 3 | -------------------------------------------------------------------------------- /app/javascript/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrocket/rails-template/HEAD/app/javascript/.DS_Store -------------------------------------------------------------------------------- /spec/template.rb: -------------------------------------------------------------------------------- 1 | copy_file "spec/support/factory_bot.rb" 2 | copy_file "spec/factories.rb" 3 | apply "spec/rails_helper.rb" 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/active_admin.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #fc0; 2 | @import 'activeadmin_addons/all'; 3 | @import "arctic_admin/base"; 4 | -------------------------------------------------------------------------------- /spec/support/factory_bot.rb: -------------------------------------------------------------------------------- 1 | require "factory_bot_rails" 2 | 3 | RSpec.configure do |config| 4 | config.include FactoryBot::Syntax::Methods 5 | end 6 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | rails: bundle exec rails s -p 3000 -b 0.0.0.0 2 | tailwind: rails tailwindcss:watch 3 | sidekiq: bundle exec sidekiq -e development -C config/sidekiq.yml -------------------------------------------------------------------------------- /k8s/sidekiq_quite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Find Pid 4 | SIDEKIQ_PID=$(ps aux | grep sidekiq | grep busy | awk '{ print $2 }') 5 | # Send TSTP signal 6 | kill -SIGTSTP $SIDEKIQ_PID -------------------------------------------------------------------------------- /app/jobs/http_post_job.rb: -------------------------------------------------------------------------------- 1 | class HttpPostJob < ApplicationJob 2 | queue_as :default 3 | include HttpHelper 4 | 5 | def perform(url, options = {}) 6 | post(url, options) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/errors_controller.rb: -------------------------------------------------------------------------------- 1 | class ErrorsController < ApplicationController 2 | def show 3 | status_code = params[:code] || 500 4 | render status_code.to_s, status: status_code 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/errors/404.html.erb: -------------------------------------------------------------------------------- 1 |
#{instance.error_message.uniq.join(', ')}
).html_safe 25 | else 26 | html = %(#{e}#{instance.error_message}
).html_safe 27 | end 28 | end 29 | end 30 | html 31 | end 32 | RUBY 33 | end 34 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | gsub_file "config/environments/development.rb", "config.cache_store = :memory_store", <<-'RUBY' 2 | config.cache_store = :redis_cache_store, { 3 | url: "redis://localhost:6379/2", 4 | pool_size: 5, # https://guides.rubyonrails.org/caching_with_rails.html#connection-pool-options 5 | pool_timeout: 3, 6 | connect_timeout: 10, # Defaults to 20 seconds 7 | read_timeout: 1, # Defaults to 1 second 8 | write_timeout: 1, # Defaults to 1 second 9 | reconnect_attempts: 3, # Defaults to 0 10 | reconnect_delay: 0.1, 11 | reconnect_delay_max: 0.2, 12 | error_handler: ->(method:, returning:, exception:) { 13 | Sentry.capture_exception exception, level: "warning", 14 | tags: {method: method, returning: returning} 15 | } 16 | } 17 | RUBY 18 | 19 | insert_into_file "config/environments/development.rb", before: /^end/ do 20 | <<-'RUBY' 21 | config.kredis.connector = ->(config) { ConnectionPool::Wrapper.new(size: 5, timeout: 3) { Redis.new(config) } } # redis from shared.yml 22 | 23 | config.action_mailer.default_url_options = {host: "http://localhost:3000"} 24 | config.action_mailer.asset_host = 'http://localhost:3000' 25 | config.action_mailer.delivery_method = :letter_opener 26 | config.action_mailer.perform_deliveries = true 27 | RUBY 28 | end 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rails-template 2 | 3 | ## Description 4 | rails template for kubernetes deployment 5 | 6 | [see generated sample](https://github.com/astrocket/rails-template-stimulus) 7 | 8 | ## Requirements 9 | * Rails 7.x (w/tailwind) 10 | * Ruby 3.x 11 | 12 | ## Installation 13 | 14 | To generate a Rails application using this template, pass the `-m` option to `rails new`, like this: 15 | 16 | ```bash 17 | $ rails new project -T -d postgresql --css tailwind \ 18 | -m https://raw.githubusercontent.com/astrocket/rails-template/master/template.rb 19 | ``` 20 | 21 | ## What's included? 22 | 23 | * Kubernetes & Docker for production deploy 24 | * Tailwind & Stimulus by importmaps 25 | * ActiveJob, Sidekiq, Redis setting for background job 26 | * ActiveAdmin + ArcticAdmin for application admin 27 | * Rspec + FactoryBot setting for test code 28 | 29 | ## Foreman start task 30 | 31 | Procfile based applications 32 | 33 | with `foreman start` 34 | 35 | It runs 36 | 37 | * rails 38 | * tailwind 39 | * sidekiq 40 | 41 | ## Stimulus.js generator 42 | 43 | Stimulus specific generator task. 44 | 45 | with `rails g stimulus posts index` 46 | 47 | It generates 48 | 49 | * `app/javascript/posts/index_controller.js` with sample html markup containing stimulus path helper. 50 | 51 | ## Kubernetes & Docker 52 | 53 | With [kubernetes](https://kubernetes.io/) you can manage multiple containers with simple `yaml` files. 54 | 55 | Template contains 56 | 57 | * deployment [guide](k8s/README.md.tt) for DigitalOcean's cluster from scratch 58 | * Let's Encrypt issuer and Ingress configuration 59 | * demo deployment yaml to instantly run sample hashicorp/http-echo + nginx app 60 | * basic puma, nginx and sidekiq deployment setup 61 | 62 | ## Testing 63 | 64 | [rspec-rails](https://github.com/rspec/rspec-rails) 65 | 66 | [factory-bot](https://github.com/thoughtbot/factory_bot/wiki) 67 | 68 | > Run test specs 69 | ```bash 70 | bundle exec rspec 71 | ``` -------------------------------------------------------------------------------- /app/lib/http_helper.rb: -------------------------------------------------------------------------------- 1 | require "uri" 2 | require "net/http" 3 | require "net/https" 4 | require "json" 5 | require "time" 6 | 7 | module HttpHelper 8 | def self.included(base) 9 | base.extend(Methods) 10 | base.send(:include, Methods) 11 | end 12 | 13 | module Methods 14 | def get_json(url, options = {}) 15 | JSON.parse(get(url, options).body) 16 | end 17 | 18 | def post_json(url, options = {}) 19 | JSON.parse(post(url, options).body) 20 | end 21 | 22 | def delete_json(url, options = {}) 23 | JSON.parse(delete(url, options).body) 24 | end 25 | 26 | def get(url, options = {}) 27 | options = { 28 | headers: false, 29 | timeout: 10 30 | }.merge!(options) 31 | 32 | uri = URI.parse(url) 33 | https = Net::HTTP.new(uri.host, uri.port) 34 | https.read_timeout = options[:timeout] || 10 35 | https.use_ssl = url.include?("https") 36 | req = Net::HTTP::Get.new(uri) 37 | options[:headers].map { |k, v| req[k] = v } if options[:headers] 38 | https.request(req) 39 | end 40 | 41 | def post(url, options = {}) 42 | options = { 43 | body: {}, 44 | headers: { 45 | "Content-Type" => "application/json" 46 | }, 47 | timeout: 10 48 | }.merge!(options) 49 | 50 | uri = URI.parse(url) 51 | https = Net::HTTP.new(uri.host, uri.port) 52 | https.read_timeout = options[:timeout] || 10 53 | https.use_ssl = url.include?("https") 54 | req = Net::HTTP::Post.new(uri.path, initheader = options[:headers]) 55 | req.body = options[:body].to_json 56 | https.request(req) 57 | end 58 | 59 | def delete(url, options = {}) 60 | options = { 61 | body: {}, 62 | headers: { 63 | "Content-Type" => "application/json" 64 | }, 65 | timeout: 10 66 | }.merge!(options) 67 | 68 | uri = URI.parse(url) 69 | https = Net::HTTP.new(uri.host, uri.port) 70 | https.read_timeout = options[:timeout] || 10 71 | https.use_ssl = url.include?("https") 72 | req = Net::HTTP::Delete.new(uri.path, initheader = options[:headers]) 73 | req.body = options[:body].to_json 74 | https.request(req) 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/tasks/deploy.rake.tt: -------------------------------------------------------------------------------- 1 | require "io/console" 2 | require "bundler/vendor/thor/lib/thor/shell" 3 | extend Bundler::Thor::Shell 4 | 5 | def terminal_length 6 | require "io/console" 7 | IO.console.winsize[1] 8 | rescue LoadError 9 | Integer(`tput co`) 10 | end 11 | 12 | def full_liner(string) 13 | remaining_length = terminal_length - string.length - 1 14 | "#{string} " + ("-" * remaining_length).to_s 15 | end 16 | 17 | def log_containers(app) 18 | puts set_color("Please type one of your #{app}'s container names", :blue, :bold) 19 | 20 | names = `kubectl get pods -l app=#{app} -o jsonpath='{.items[0].spec.containers[*].name}'`.split(" ") 21 | names_table = {} 22 | names.each_with_index do |name, _i| 23 | names_table[_i] = name 24 | puts set_color("[#{_i}] #{name}", :green) 25 | end 26 | 27 | names_i = STDIN.gets.strip.to_i 28 | sh("kubectl logs -f --tail=5 --selector app=#{app} -c #{names_table[names_i]}") 29 | end 30 | 31 | namespace :deploy do 32 | namespace :demo do 33 | task up: :environment do 34 | sh("kubectl apply -f k8s/demo.yaml") 35 | end 36 | 37 | task down: :environment do 38 | sh("kubectl delete -f k8s/demo.yaml") 39 | end 40 | end 41 | 42 | namespace :production do 43 | task set_master_key: :environment do 44 | sh("kubectl create secret generic <%= k8s_name %>-secrets --from-file=<%= k8s_name %>-master-key=config/master.key") 45 | end 46 | 47 | namespace :ingress do 48 | task up: :environment do 49 | sh("kubectl apply -f k8s/project/<%= k8s_name %>-nginx-conf.yaml") 50 | sh("kubectl apply -f k8s/service.yaml") 51 | sh("kubectl apply -f k8s/ingress.yaml") 52 | end 53 | 54 | task down: :environment do 55 | sh("kubectl delete secret <%= k8s_name %>-secrets") 56 | sh("kubectl delete -f k8s/project/<%= k8s_name %>-nginx-conf.yaml") 57 | sh("kubectl delete -f k8s/service.yaml") 58 | sh("kubectl delete -f k8s/ingress.yaml") 59 | end 60 | end 61 | end 62 | 63 | namespace :logs do 64 | task demo: :environment do 65 | log_containers("<%= k8s_name %>-demo-web") 66 | end 67 | 68 | task web: :environment do 69 | log_containers("<%= k8s_name %>-web") 70 | end 71 | 72 | task sidekiq: :environment do 73 | log_containers("<%= k8s_name %>-sidekiq") 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /app/javascript/utils/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | function getQueryString(params) { 4 | return Object.keys(params) 5 | .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) 6 | .join('&'); 7 | } 8 | 9 | const get = (path, params) => { 10 | const qs = '?' + getQueryString(params || {}); 11 | const headers = { 12 | 'Cache-Control': 'no-cache', 13 | 'Pragma': 'no-cache', 14 | 'Expires': '0' 15 | } 16 | 17 | return axios({ 18 | method: 'get', 19 | headers: headers, 20 | url: path + qs 21 | }) 22 | }; 23 | 24 | const post = (path, params, headers = null) => { 25 | headers = headers || { 26 | 'Accept': 'application/json', 27 | 'Content-Type': 'application/json', 28 | 'Cache-Control': 'no-cache', 29 | 'Pragma': 'no-cache', 30 | 'Expires': '0' 31 | }; 32 | headers['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); 33 | 34 | return axios({ 35 | method: 'post', 36 | url: path, 37 | headers: headers, 38 | data: params 39 | }) 40 | }; 41 | 42 | const put = (path, params, headers = null) => { 43 | headers = headers || { 44 | 'Accept': 'application/json', 45 | 'Content-Type': 'application/json', 46 | 'Cache-Control': 'no-cache', 47 | 'Pragma': 'no-cache', 48 | 'Expires': '0' 49 | }; 50 | headers['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); 51 | 52 | return axios({ 53 | method: 'put', 54 | url: path, 55 | headers: headers, 56 | data: params 57 | }) 58 | }; 59 | 60 | const patch = (path, params, headers = null) => { 61 | headers = headers || { 62 | 'Accept': 'application/json', 63 | 'Content-Type': 'application/json', 64 | 'Cache-Control': 'no-cache', 65 | 'Pragma': 'no-cache', 66 | 'Expires': '0' 67 | }; 68 | headers['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); 69 | 70 | return axios({ 71 | method: 'patch', 72 | url: path, 73 | headers: headers, 74 | data: params 75 | }) 76 | }; 77 | 78 | export default { 79 | get: get, 80 | post: post, 81 | put: put, 82 | patch: patch 83 | } 84 | -------------------------------------------------------------------------------- /k8s/sidekiq.yaml.tt: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: <%= k8s_name %>-sidekiq 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: <%= k8s_name %>-sidekiq 9 | replicas: 1 10 | strategy: 11 | type: RollingUpdate 12 | rollingUpdate: 13 | maxSurge: 1 14 | maxUnavailable: 0% 15 | template: 16 | metadata: 17 | labels: 18 | app: <%= k8s_name %>-sidekiq 19 | spec: 20 | imagePullSecrets: 21 | - name: digitalocean-access-token 22 | containers: 23 | - name: sidekiq 24 | image: <%= container_registry_path %>:$IMAGE_TAG 25 | imagePullPolicy: Always 26 | command: ["/bin/sh","-c"] 27 | args: ["bundle exec sidekiq -C config/sidekiq.yml"] 28 | env: 29 | - name: RAILS_ENV 30 | value: "production" 31 | - name: RAILS_LOG_TO_STDOUT 32 | value: "true" 33 | - name: REDIS_URL 34 | value: "redis://<%= k8s_name %>-redis-svc:6379" 35 | - name: DEPLOY_VERSION 36 | value: $IMAGE_TAG 37 | - name: RAILS_MASTER_KEY 38 | valueFrom: 39 | secretKeyRef: 40 | name: <%= k8s_name %>-secrets 41 | key: <%= k8s_name %>-master-key 42 | resources: 43 | requests: 44 | cpu: 600m 45 | memory: 800Mi 46 | limits: 47 | cpu: 700m 48 | memory: 900Mi 49 | ports: 50 | - containerPort: 7433 51 | livenessProbe: 52 | httpGet: 53 | path: / 54 | port: 7433 55 | initialDelaySeconds: 15 56 | timeoutSeconds: 5 57 | readinessProbe: 58 | httpGet: 59 | path: / 60 | port: 7433 61 | initialDelaySeconds: 15 62 | periodSeconds: 5 63 | successThreshold: 2 64 | failureThreshold: 2 65 | timeoutSeconds: 5 66 | lifecycle: 67 | preStop: 68 | exec: 69 | command: ["k8s/sidekiq_quiet"] 70 | terminationGracePeriodSeconds: 300 71 | --- 72 | apiVersion: autoscaling/v2beta1 73 | kind: HorizontalPodAutoscaler 74 | metadata: 75 | name: <%= k8s_name %>-sidekiq 76 | spec: 77 | scaleTargetRef: 78 | apiVersion: apps/v1 79 | kind: Deployment 80 | name: <%= k8s_name %>-sidekiq 81 | minReplicas: 1 82 | maxReplicas: 2 83 | metrics: 84 | - type: Resource 85 | resource: 86 | name: cpu 87 | targetAverageUtilization: 60 -------------------------------------------------------------------------------- /k8s/project/application-nginx-conf.yaml.tt: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: <%= k8s_name %>-nginx-conf 5 | data: 6 | nginx.conf: | 7 | user nginx; 8 | worker_processes auto; 9 | 10 | error_log /var/log/nginx/error.log warn; 11 | pid /var/run/nginx.pid; 12 | 13 | events { 14 | worker_connections 16384; 15 | } 16 | 17 | http { 18 | include /etc/nginx/mime.types; 19 | default_type application/octet-stream; 20 | 21 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 22 | '$status $body_bytes_sent "$http_referer" ' 23 | '"$http_user_agent" "$http_x_forwarded_for"'; 24 | 25 | access_log /dev/stdout main; 26 | # access_log off; 27 | 28 | sendfile on; 29 | tcp_nopush on; 30 | tcp_nodelay on; 31 | 32 | gzip on; 33 | gzip_static on; 34 | gzip_http_version 1.0; 35 | gzip_comp_level 2; 36 | gzip_min_length 1000; 37 | gzip_proxied any; 38 | gzip_types application/x-javascript text/css text/javascript text/plain text/xml image/x-icon image/png; 39 | gzip_vary on; 40 | gzip_disable "MSIE [1-6].(?!.*SV1)"; 41 | 42 | keepalive_timeout 45; 43 | client_max_body_size 20m; 44 | 45 | upstream app { 46 | server 127.0.0.1:3000 fail_timeout=0; 47 | } 48 | 49 | server { 50 | listen 80; 51 | 52 | root /assets; 53 | 54 | location /nginx_status { 55 | stub_status on; 56 | access_log off; 57 | allow 127.0.0.1; 58 | deny all; 59 | } 60 | 61 | location ~ ^/assets/ { 62 | expires 60d; 63 | add_header Cache-Control public; 64 | add_header ETag ""; 65 | break; 66 | } 67 | 68 | location /admin { 69 | client_max_body_size 100m; 70 | try_files $uri/index.html $uri/index.htm @app; 71 | } 72 | 73 | location / { 74 | try_files $uri/index.html $uri/index.htm @app; 75 | } 76 | 77 | location @app { 78 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 79 | proxy_set_header Host $http_host; 80 | proxy_http_version 1.1; 81 | proxy_redirect off; 82 | 83 | proxy_read_timeout 30; 84 | proxy_send_timeout 30; 85 | 86 | # If you don't find the filename in the static files 87 | # Then request it from the app server 88 | if (!-f $request_filename) { 89 | proxy_pass http://app; 90 | break; 91 | } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /app/views/home/index.html.erb: -------------------------------------------------------------------------------- 1 |"Nullam ut vulputate orci. Duis eleifend urna nec."
8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ut vulputate orci. Duis eleifend urna nec lacus 9 | faucibus ultricies. Aliquam in mattis justo. Nunc sollicitudin blandit venenatis. Nam rutrum, urna at hendrerit 10 | aliquam, nisl nibh pretium lorem, vel fringilla risus dolor id tortor. Vestibulum massa massa, scelerisque vitae 11 | cursus a, ultricies id orci. Pellentesque vel efficitur felis, in aliquam neque. Sed semper sed sem vel fermentum. 12 | Curabitur eget tortor ut nisi hendrerit blandit. Suspendisse semper feugiat neque nec imperdiet. Maecenas 13 | vulputate fringilla lectus ut dictum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur 14 | ridiculus mus. Mauris accumsan tortor a lacus consectetur, et dignissim ante venenatis. Vivamus sit amet aliquam 15 | tortor, vel vehicula turpis. Proin posuere nibh in lorem rutrum, a elementum leo dignissim. Vestibulum massa 16 | ipsum, venenatis id leo nec, luctus efficitur mi. 17 |
18 |19 | Integer porta pretium turpis ut gravida. Suspendisse efficitur lorem accumsan ante tincidunt maximus et vel quam. 20 | Morbi nec mattis metus. Nullam aliquet enim et ultricies rhoncus. Phasellus eleifend tempor erat at tempor. Donec 21 | dapibus facilisis orci, nec suscipit metus sollicitudin sit amet. In elementum ligula a fermentum accumsan. Aenean 22 | metus diam, accumsan sit amet hendrerit id, rutrum nec diam. Suspendisse ligula turpis, vulputate in malesuada 23 | aliquet, interdum quis ipsum. Mauris hendrerit mauris vel tincidunt volutpat. 24 |
25 |26 | Nam imperdiet maximus ultricies. Curabitur rhoncus sollicitudin posuere. Sed semper sagittis placerat. Mauris vel 27 | leo mauris. Ut urna justo, pulvinar sed elit non, imperdiet vehicula augue. Aenean et porttitor nisl, ac mollis 28 | tellus. Morbi cursus enim non scelerisque interdum. Sed luctus justo eu dapibus congue. Sed euismod interdum ante 29 | eu blandit. Duis tortor arcu, molestie eget egestas sed, tempus ac urna. 30 |
31 | <% end %> 32 |