├── .editorconfig ├── .github └── workflows │ ├── codeql-analysis.yml │ ├── gem-push.yml │ └── ruby.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── deer-jekyll-strapi-4.png ├── docs ├── .gitignore ├── 404.html ├── Gemfile ├── _config.yml ├── assets │ └── images │ │ ├── jekyll-strapi-ng.drawio.png │ │ ├── s-00.jpg │ │ ├── s-01.jpg │ │ ├── s-02.jpg │ │ ├── s-03.jpg │ │ ├── s-04.jpg │ │ ├── s-05.jpg │ │ └── s-07.jpg ├── dev.markdown ├── docs.markdown └── index.markdown ├── jekyll-strapi-4.gemspec ├── jekyll-strapi-ng.drawio ├── lib ├── jekyll-strapi-4.rb └── jekyll │ ├── strapi4 │ ├── collection.rb │ ├── collection_permalink.rb │ ├── drops.rb │ ├── generator.rb │ ├── hooks.rb │ ├── site.rb │ ├── strapihttp.rb │ └── version.rb │ └── tags │ └── strapiimagefilter.rb └── test ├── _layouts └── post.html ├── source └── _data │ ├── photo.01.json │ └── photos.json ├── test_collection.rb ├── test_hello.rb └── test_strapi_page.rb /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{rb,gemspec}] 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '40 5 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'ruby' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/gem-push.yml: -------------------------------------------------------------------------------- 1 | name: Ruby Gem 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | name: Build + Publish 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | packages: write 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up Ruby 2.6 20 | uses: actions/setup-ruby@v1 21 | with: 22 | ruby-version: 2.6.x 23 | 24 | - name: Publish to GPR 25 | run: | 26 | mkdir -p $HOME/.gem 27 | touch $HOME/.gem/credentials 28 | chmod 0600 $HOME/.gem/credentials 29 | printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 30 | gem build *.gemspec 31 | gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem 32 | env: 33 | GEM_HOST_API_KEY: "Bearer ${{secrets.GITHUB_TOKEN}}" 34 | OWNER: ${{ github.repository_owner }} 35 | 36 | - name: Publish to RubyGems 37 | run: | 38 | mkdir -p $HOME/.gem 39 | touch $HOME/.gem/credentials 40 | chmod 0600 $HOME/.gem/credentials 41 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 42 | gem build *.gemspec 43 | gem push *.gem 44 | env: 45 | GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}" 46 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Ruby 9 | 10 | on: 11 | push: 12 | branches: [ "master", "develop" ] 13 | pull_request: 14 | branches: [ "master", "develop" ] 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | test: 21 | 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | ruby-version: ['3.0'] 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Set up Ruby 30 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, 31 | # change this to (see https://github.com/ruby/setup-ruby#versioning): 32 | # uses: ruby/setup-ruby@v1 33 | uses: ruby/setup-ruby@2b019609e2b0f1ea1a2bc8ca11cb82ab46ada124 34 | with: 35 | ruby-version: ${{ matrix.ruby-version }} 36 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 37 | - name: Run tests 38 | run: bundle exec rake 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.gem 4 | .jekyll-cache 5 | .env 6 | Gemfile.lock 7 | rdoc 8 | _tmp_assets 9 | _site 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | 3 | * Release in Ruby Gems as jekyll-strapi-4 4 | 5 | 0.4.3.1-dev 6 | 7 | * Translation for permalinks 8 | * Slugs 9 | * Custom parameters 10 | 11 | 0.4.2-dev 12 | 13 | * GitHub Package 14 | * GitHub Actions 15 | 16 | 0.4.1-dev 17 | 18 | * Basic compatibility with Strapi 4 (Collections are working, Single Type not yet) Strapi incompatible with jekyll 4.0.0 #8 19 | * Authentication (tested with Content API Token and Personal Token) 20 | * Filter to fetch media object types from the Strapi 4 Instance 21 | * Better error handling (distinguish 401 from 403, and handle 404 more accurate) 22 | * Extra custom parameters - I believe it solves Sorting collection data #15 23 | * Unttests with 'mock/fixtures' 24 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec :name => "jekyll-strapi-4" 3 | 4 | gem "rake", "~> 13.0" 5 | gem "test-unit" 6 | gem "minima" 7 | 8 | gem "webrick", "~> 1.7" 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Rafał Zawadzki, Michał Krajewski 2 | 3 | Copyright (c) 2015-2019 Strapi Solutions. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jekyll-strapi-4 2 | 3 | This plugin works with Strapi 4 and is based on [jekyll-strapi](https://github.com/strapi-community/jekyll-strapi/tree/v0.1.2) plugin for Strapi 3. 4 | 5 | ![](deer-jekyll-strapi-4.png?raw=true) 6 | 7 | Q: Why the Deer for logo? 8 | 9 | A: Every project deserves to have the cute deer as a logo. 10 | 11 | ## Features 12 | 13 | * Support for Strapi 4 14 | * Authentication 15 | * Permalinks 16 | * Caching and collecting assets from Strapi 17 | * Added UnitTests 18 | * Documentation in Jekyll format 19 | 20 | ## Install 21 | 22 | Add the "jekyll-strapi-4" gem to your Gemfile: 23 | 24 | ``` 25 | gem "jekyll-strapi-4" 26 | ``` 27 | 28 | Then add "jekyll-strapi-4" to your plugins in `_config.yml`: 29 | 30 | ``` 31 | plugins: 32 | - jekyll-strapi-4 33 | ``` 34 | 35 | ## Configuration 36 | 37 | ```yaml 38 | strapi: 39 | # Your API endpoint (optional, default to http://localhost:1337) 40 | endpoint: http://localhost:1337 41 | # Collections, key is used to access in the strapi.collections 42 | # template variable 43 | collections: 44 | # Example for a "Photo" collection 45 | photos: 46 | # Collection name (optional) 47 | # type: photos 48 | # Permalink used to generate the output files (eg. /articles/:id). 49 | permalink: /photos/:id/ 50 | # Permalinks defined for different locales 51 | permalinks: 52 | pl: "/zdjecia/:id" 53 | # Parameters (optional) 54 | parameters: 55 | sort: title:asc 56 | pagination[pageSize]: 10 57 | # Populate page (optional, default "*") 58 | # populate: "*" 59 | # Layout file for this collection 60 | layout: photo.html 61 | # Single request for collection, default false 62 | single_request: true 63 | # Generate output files or not (default: false) 64 | output: true 65 | ``` 66 | 67 | This works for the following collection *Photo* in Strapi: 68 | 69 | | Name | Type | 70 | | ------- | ----- | 71 | | Title | Text | 72 | | Image | Media | 73 | | Comment | Text | 74 | 75 | ### Authentication 76 | 77 | To access non Public collections (and by default all Strapi collections are non Public) you must to generate a token inside your strapi instance and set it as enviromental variable `STRAPI_TOKEN`. 78 | 79 | It is recommended that you will use new Content API tokens for this task: https://strapi.io/blog/a-beginners-guide-to-authentication-and-authorization-in-strapi 80 | 81 | ## Usage 82 | 83 | This plugin provides the `strapi` template variable. This template provides access to the collections defined in the configuration. 84 | 85 | ### Using Collections 86 | 87 | Collections are accessed by their name in `strapi.collections`. The `articles` collections is available at `strapi.collections.articles`. 88 | 89 | To list all documents of the collection ```_layouts/home.html```: 90 | 91 | ``` 92 | --- 93 | layout: default 94 | --- 95 |
96 |

Photos

97 | {%- if strapi.collections.photos.size > 0 -%} 98 | 105 | {%- endif -%} 106 |
107 | ``` 108 | 109 | ### Attributes 110 | 111 | All object's data you can access through ``` {{ page.document.strapi_attributes }}```. This plugin also introduces new filter asset_url which perform downloading the asset into the assets folder and provides correct url. Thanks for this you have copies of your assets locally without extra dependency on Strapi ```_layouts/photo.html```: 112 | 113 | ``` 114 | --- 115 | layout: default 116 | --- 117 | 118 |
119 |

{{ page.document.title }}

120 |

{{ page.document.strapi_attributes.Title }}

121 |

{{ page.document.strapi_attributes.Comment }}

122 | 123 |
124 | ``` 125 | 126 | ### Request parameters 127 | 128 | Define your request parameters in config files (check configuration). 129 | 130 | If you want to add custom logic use custom_path_params method. You can use it by create _plugins/filename.rb. 131 | 132 | ```ruby 133 | module Jekyll 134 | module Strapi 135 | class StrapiCollection 136 | def custom_path_params 137 | # ex. for multilanguage plugin you might want get request by page lang 138 | "&locale=#{@site.config["lang"]}" 139 | end 140 | end 141 | end 142 | end 143 | ``` 144 | 145 | ### Single request 146 | 147 | When you request for a collection it makes a collection request and collection resources request. When you have a small collection like testimonials you might not make n+1 requests but only one. In that case use single_request: true in your _config file. 148 | 149 | ```yaml 150 | strapi: 151 | collections: 152 | photos: 153 | single_request: true 154 | ``` 155 | 156 | You can always add to your parameters populate parameter to get additional data in your collection request. 157 | 158 | ### Permalinks 159 | 160 | When you have a multi-language content, you might want generate a proper url based on different patterns, for example: 161 | | Language | permalink pattern | example | 162 | | ----------- | ----------- | - | 163 | | en | /image/:slug |yourdomain.com/image/orange/ | 164 | | es | /imagen/:slug | yourdomain.com/imagen/naranja/ | 165 | | pl | /zdjecie/:slug | yourdomain.com/zdjecie/pomarancza/ | 166 | 167 | In that case you have to 168 | 1. set locales [on request parameters](#request-parameters), 169 | 2. set permalinks patterns [on _config.yml](#configuration). 170 | 171 | When you create permalinks, set default permalink and optionals permalinks. 172 | 173 | ```yaml 174 | strapi: 175 | collections: 176 | photos: 177 | permalink: /image/:slug/ 178 | permalinks: 179 | es: /imagen/:slug 180 | pl: /zdjecia/:slug 181 | 182 | ``` 183 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | ## 2 | # Rakefile https://ruby-doc.org/stdlib-3.1.2/libdoc/rake/rdoc/Rake/TestTask.html 3 | multitask :default => [:test] 4 | 5 | require "rake/testtask" 6 | Rake::TestTask.new do |t| 7 | t.libs << "test" 8 | t.test_files = FileList['test/test*.rb'] 9 | t.verbose = true 10 | end 11 | 12 | require "rdoc/task" 13 | Rake::RDocTask.new do |rdoc| 14 | rdoc.rdoc_dir = "rdoc" 15 | # rdoc.title = "#{name} #{version}" 16 | rdoc.title = "jekyll-strapi-4 1.0.7" 17 | rdoc.rdoc_files.include("README*") 18 | rdoc.rdoc_files.include("lib/**/*.rb") 19 | end 20 | 21 | desc "Build jekyll-strapi-4 into pkg/" 22 | task :build do 23 | sh "gem build" 24 | end 25 | -------------------------------------------------------------------------------- /deer-jekyll-strapi-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/jekyll-strapi/3faab9d42a8e9f4addbf5acfca56f22441ce4711/deer-jekyll-strapi-4.png -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-cache 4 | .jekyll-metadata 5 | vendor 6 | Gemfile.lock 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | # Hello! This is where you manage which Jekyll version is used to run. 3 | # When you want to use a different version, change it below, save the 4 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 5 | # 6 | # bundle exec jekyll serve 7 | # 8 | # This will help ensure the proper Jekyll version is running. 9 | # Happy Jekylling! 10 | gem "jekyll", "~> 4.2.2" 11 | # This is the default theme for new Jekyll sites. You may change this to anything you like. 12 | gem "minima", "~> 2.5" 13 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 14 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 15 | # gem "github-pages", group: :jekyll_plugins 16 | # If you have any plugins, put them here! 17 | group :jekyll_plugins do 18 | gem "jekyll-feed", "~> 0.12" 19 | end 20 | 21 | # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem 22 | # and associated library. 23 | platforms :mingw, :x64_mingw, :mswin, :jruby do 24 | gem "tzinfo", "~> 1.2" 25 | gem "tzinfo-data" 26 | end 27 | 28 | # Performance-booster for watching directories on Windows 29 | gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] 30 | 31 | # Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem 32 | # do not have a Java counterpart. 33 | gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] 34 | -------------------------------------------------------------------------------- /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: jekyll-strapi-4 22 | # email: your-email@example.com 23 | description: >- # this means to ignore newlines until "baseurl:" 24 | Jekyll plugin to get the content from Strapi v4. 25 | baseurl: "/jekyll-strapi-4" # the subpath of your site, e.g. /blog 26 | url: "https://bluszcz.github.io" # the base hostname & protocol for your site, e.g. http://example.com 27 | #twitter_username: jekyllrb 28 | github_username: bluszcz 29 | 30 | # Build settings 31 | theme: minima 32 | plugins: 33 | - jekyll-feed 34 | 35 | # Exclude from processing. 36 | # The following items will not be processed, by default. 37 | # Any item listed under the `exclude:` key here will be automatically added to 38 | # the internal "default list". 39 | # 40 | # Excluded items can be processed by explicitly listing the directories or 41 | # their entries' file path in the `include:` list. 42 | # 43 | exclude: 44 | - .sass-cache/ 45 | - .jekyll-cache/ 46 | - gemfiles/ 47 | - Gemfile 48 | - Gemfile.lock 49 | - node_modules/ 50 | - vendor/bundle/ 51 | - vendor/cache/ 52 | - vendor/gems/ 53 | - vendor/ruby/ 54 | - jekyll-strapi-ng.drawio 55 | -------------------------------------------------------------------------------- /docs/assets/images/jekyll-strapi-ng.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/jekyll-strapi/3faab9d42a8e9f4addbf5acfca56f22441ce4711/docs/assets/images/jekyll-strapi-ng.drawio.png -------------------------------------------------------------------------------- /docs/assets/images/s-00.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/jekyll-strapi/3faab9d42a8e9f4addbf5acfca56f22441ce4711/docs/assets/images/s-00.jpg -------------------------------------------------------------------------------- /docs/assets/images/s-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/jekyll-strapi/3faab9d42a8e9f4addbf5acfca56f22441ce4711/docs/assets/images/s-01.jpg -------------------------------------------------------------------------------- /docs/assets/images/s-02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/jekyll-strapi/3faab9d42a8e9f4addbf5acfca56f22441ce4711/docs/assets/images/s-02.jpg -------------------------------------------------------------------------------- /docs/assets/images/s-03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/jekyll-strapi/3faab9d42a8e9f4addbf5acfca56f22441ce4711/docs/assets/images/s-03.jpg -------------------------------------------------------------------------------- /docs/assets/images/s-04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/jekyll-strapi/3faab9d42a8e9f4addbf5acfca56f22441ce4711/docs/assets/images/s-04.jpg -------------------------------------------------------------------------------- /docs/assets/images/s-05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/jekyll-strapi/3faab9d42a8e9f4addbf5acfca56f22441ce4711/docs/assets/images/s-05.jpg -------------------------------------------------------------------------------- /docs/assets/images/s-07.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/jekyll-strapi/3faab9d42a8e9f4addbf5acfca56f22441ce4711/docs/assets/images/s-07.jpg -------------------------------------------------------------------------------- /docs/dev.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Development 4 | permalink: /dev/ 5 | --- 6 | 7 | # Development Docs 8 | 9 | ## Authentication 10 | 11 | For the authentication we are using enviromental variable `STRAPI_TOKEN` which can be one of Content API or Personal tokens. 12 | 13 | ## Fetching data 14 | 15 | This plugin works in following way - first it gets genral information about collection, and then interates over all elements using extra *populate* parameter which allows access to media files. Pseudo code: 16 | 17 | ``` 18 | collection = strapi_request() # HTTP request 19 | for elem in collection 20 | data = elem.get_data() # # HTTP request 21 | ``` 22 | ## Filters 23 | 24 | ### Copying media from Strapi 25 | 26 | In Strapi you can create Media type of field, which can contain Image, for instance. To avoid linking to Strapi instance - this plugin introduces new filter `asset_url` which fetches media file to the temporary location, and then using Jekyll internals `Jekyl::StaticFile` copies it to the output site, plus generates url link as output. -------------------------------------------------------------------------------- /docs/docs.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Docs 4 | permalink: /docs/ 5 | --- 6 | 7 | # Quick Start 8 | 9 | ## Portfolio 10 | 11 | In this example, we are creating a very simple Photography portfolio page, where users can upload a photo with a title and a simple description. 12 | 13 | ### Strapi Configuration 14 | 15 | #### CMS Setup 16 | 17 | Create new Strapi project: 18 | 19 | ``` 20 | npx create-strapi-app@latest my-project-photo --quickstart 21 | ``` 22 | 23 | And then to start project: 24 | 25 | ``` 26 | cd my-project-photo 27 | npm run develop 28 | ``` 29 | 30 | Or if you are using yarn: 31 | ``` 32 | yarn create-strapi-app@latest my-project-photo --quickstart 33 | cd my-project-photo 34 | yarn run develop 35 | ``` 36 | 37 | Now you should have reachable Strapi instance here [http://localhost:1337/](http://localhost:1337/). 38 | 39 | 40 | #### Collection setup 41 | 42 | Go to Content-Type Build in your admin instance [http://localhost:1337/admin/plugins/content-type-builder/](http://localhost:1337/admin/plugins/content-type-builder/) and then create Collection as below: 43 | 44 | ![image](/assets/images/s-01.jpg) 45 | 46 | To creat the fields choose: 47 | 48 | **Text** field for the *Title*, type of **Short text**: 49 | 50 | 51 | ![image](/assets/images/s-02.jpg) 52 | 53 | **Media** field with a name *Image*, type **Single media**: 54 | 55 | ![image](/assets/images/s-03.jpg) 56 | 57 | **Text** *Comment* field, we will use **Long text**: 58 | 59 | ![image](/assets/images/s-04.jpg) 60 | 61 | Finally you should have something similar to: 62 | 63 | ![image](/assets/images/s-00.jpg) 64 | 65 | 66 | Now go to **Content Manager** and add your first object to the database: 67 | 68 | ![image](/assets/images/s-07.jpg) 69 | 70 | #### Auth token generation 71 | 72 | Go to: [http://localhost:1337/admin/settings/api-tokens/create](http://localhost:1337/admin/settings/api-tokens/create) and *Create new token*: 73 | 74 | ![image](/assets/images/s-05.jpg) 75 | 76 | After creation save token aside - you will need it later. Some [password manager](https://github.com/keepassxreboot/keepassxc/) is recommended. 77 | 78 | ### Plugin installation 79 | 80 | Currently new version plugins is being develop only in this repo and it is not available through RubyGems, yet. You need to download it from GitHub: 81 | 82 | ``` 83 | MAIN_PATH=`pwd` 84 | git clone https://github.com/bluszcz/jekyll-strapi-4.git 85 | cd jekyll-strapi 86 | gem build 87 | cd $MAIN_PATH 88 | ``` 89 | 90 | This is will a plugin which you will install later 91 | 92 | ### Jekyll configuration 93 | 94 | Add `jekyll-strapi-4` to the plugins in `_config.yml`: 95 | 96 | ``` 97 | plugins: 98 | - jekyll-feed 99 | - jekyll-strapi-4 100 | ``` 101 | 102 | and following at the end of `_config.yml`: 103 | 104 | ``` 105 | strapi: 106 | # Your API endpoint (optional, default to http://localhost:1337) 107 | endpoint: http://localhost:1337 108 | # Collections, key is used to access in the strapi.collections 109 | # template variable 110 | collections: 111 | # Example for a "Photo" collection 112 | photos: 113 | # Collection name (optional) 114 | # type: photos 115 | # Permalink used to generate the output files (eg. /articles/:id). 116 | permalink: /photos/:id/ 117 | # Layout file for this collection 118 | layout: photo.html 119 | # Generate output files or not (default: false) 120 | output: true 121 | ``` 122 | 123 | We install the plugin: 124 | 125 | ``` 126 | gem install $MAIN_PATH/jekyll-strapi/jekyll-strapi-0.4.1.pre.dev.gem 127 | rm Gemfile.lock 128 | bundle install 129 | ``` 130 | 131 | Then in `_layouts` directory create two files, `home.html`: 132 | 133 | ``` 134 | --- 135 | layout: default 136 | --- 137 | {% raw %} 138 |
139 |

Photos

140 | {%- if strapi.collections.photos.size > 0 -%} 141 | 148 | {%- endif -%} 149 |
150 | {% endraw %} 151 | ``` 152 | 153 | and `photo.html`: 154 | 155 | ``` 156 | --- 157 | layout: default 158 | --- 159 | {% raw %} 160 |
161 |

{{ page.strapi_attributes.TestDescription }}

162 |

{{ page.document.strapi_attributes.Title }}

163 |

{{ page.document.strapi_attributes.Comment }}

164 | 165 |
166 | {% endraw %} 167 | 168 | ``` 169 | 170 | Now you must to set enviromental variable with auth token (you need to use previously saved token here): 171 | 172 | ``` 173 | export STRAPI_TOKEN=328438953489534...345423053895 174 | ``` 175 | 176 | and now you can generate your page: 177 | 178 | ``` 179 | bundle exec jekyll build --trace 180 | ``` 181 | 182 | And after that you can check your website: 183 | 184 | ``` 185 | cd _site 186 | python3 -m http.server 187 | ``` 188 | 189 | and opening [http://localhost:8000](http://localhost:8000) in your browser. 190 | 191 | ## Deployed example - demo 192 | 193 | Here you can see page from previously example deployed to GitHub pages: 194 | 195 | [https://jekyll-strapi-v4-example.bluszcz.net/](https://jekyll-strapi-v4-example.bluszcz.net/) 196 | 197 | using following GitHub repository: [https://github.com/bluszcz/jekyll-strapi-v4-example.github.io/](https://github.com/bluszcz/jekyll-strapi-v4-example.github.io/) -------------------------------------------------------------------------------- /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 | 8 | jekyll-strapi is a Jekyll plugin to generate static sites using Strapi 4 headless cms. 9 | 10 | # Features 11 | * Compatibility with Strapi 4 12 | * Scallable iterative model of fetching data using populate=* 13 | * Authentication using Personal and Content API tokens 14 | * Filter to fetch media files to avoid linking with original Strapi instance 15 | * Basic support for permalinks 16 | 17 | 18 | # Roadmap 19 | 20 | * Support for SingleType (2022-Q3) 21 | * Pagination (2022-Q3) 22 | * Configuration of necessary fields instead of using populate=* for all the results 23 | 24 | # Use case scenarios 25 | 26 | ## Family/friends blog with recipes 27 | 28 | A group of friends or family members would like to set up a blog with recipes. They can deploy one instance ([Strapi recommends some hosting](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/deployment.html)), create a few collections, and start work together. They can store files in Git repository and With the help of CI/CD - deploy when the new entries appear. 29 | 30 | 31 | ## Personal blog/portfolio 32 | 33 | This setup assumes Strapi running locally on your laptop, where you create the content of your personal blog and portfolio. You run Jekyll to render the pages and deploy them to Heroku, Gitlab/Github pages, or similar services. 34 | 35 | ## Multinational company 36 | 37 | ![image](/assets/images/jekyll-strapi-ng.drawio.png) 38 | 39 | Let’s imagine a company with an online presence in several countries. There would be one Strapi4 instance (which can be hosted in a private cloud) with several users “editors” from various countries. Each of them would have access to the only set of Collections where they would maintain all the data. Then, each country runs its version jekyll-strapi-ng and generates necessary static pages which can be easily deployed. Thanks to the scalable design of jekyll-strapi-ng you can generate pages from Collections containing a LOT of data. 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /jekyll-strapi-4.gemspec: -------------------------------------------------------------------------------- 1 | $:.unshift(File.expand_path("../lib", __FILE__)) 2 | require "jekyll/strapi4/version" 3 | 4 | Gem::Specification.new do |spec| 5 | spec.version = Jekyll::Strapi::VERSION 6 | spec.homepage = "https://github.com/bluszcz/jekyll-strapi-4" 7 | spec.authors = ["Strapi Solutions", "Rafał Zawadzki", "Michał Krajewski"] 8 | spec.email = ["bluszcz@bluszcz.net"] 9 | spec.files = %W(README.md LICENSE) + Dir["lib/**/*"] 10 | spec.summary = "Strapi.io integration for Jekyll" 11 | spec.required_ruby_version = '>= 3.0.0' 12 | spec.name = "jekyll-strapi-4" 13 | spec.license = "MIT" 14 | spec.require_paths = ["lib"] 15 | spec.description = spec.description = <<-DESC 16 | A Jekyll plugin for retrieving content from a Strapi 4 API 17 | DESC 18 | 19 | spec.add_runtime_dependency("down", "~> 5.0") 20 | spec.add_runtime_dependency("jekyll", "~> 4") 21 | spec.add_runtime_dependency("http", "~> 3.2") 22 | spec.add_runtime_dependency("json", "~> 2.1") 23 | 24 | end 25 | -------------------------------------------------------------------------------- /jekyll-strapi-ng.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /lib/jekyll-strapi-4.rb: -------------------------------------------------------------------------------- 1 | require 'jekyll/strapi4/strapihttp' 2 | require 'jekyll/strapi4/collection_permalink' 3 | require 'jekyll/strapi4/collection' 4 | require 'jekyll/strapi4/drops' 5 | require 'jekyll/strapi4/generator' 6 | require 'jekyll/strapi4/hooks' 7 | require 'jekyll/strapi4/site' 8 | require 'jekyll/strapi4/version' 9 | require 'jekyll/tags/strapiimagefilter' 10 | -------------------------------------------------------------------------------- /lib/jekyll/strapi4/collection.rb: -------------------------------------------------------------------------------- 1 | require "net/http" 2 | require "ostruct" 3 | require "json" 4 | 5 | 6 | 7 | 8 | 9 | module Jekyll 10 | module Strapi 11 | class StrapiCollection 12 | attr_accessor :collection_name, :config 13 | 14 | def initialize(site, collection_name, config) 15 | @site = site 16 | @collection_name = collection_name 17 | @config = config 18 | Jekyll.logger.debug "Jekyll Collection init:" "#{site.config} #{collection_name} #{config}" 19 | end 20 | 21 | def generate? 22 | @config['output'] || false 23 | end 24 | 25 | def get_data 26 | # path = "/#{@config['type'] || @collection_name}?_limit=10000" 27 | # This seems be not working anymore: 28 | # https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest-api.html#api-parameters 29 | # and pagination is now done in following way: 30 | # https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest/sort-pagination.html#pagination-by-page 31 | uri = URI("#{@site.endpoint}/api/#{endpoint}#{path_params}") 32 | Jekyll.logger.debug "StrapiCollection get_document:" "#{collection_name} #{uri}" 33 | response = strapi_request(uri) 34 | response.data 35 | end 36 | 37 | def get_document(did) 38 | uri_document = URI("#{@site.endpoint}/api/#{endpoint}/#{did}?populate=#{populate}") 39 | Jekyll.logger.debug "StrapiCollection iterating uri_document:" "#{uri_document}" 40 | strapi_request(uri_document) 41 | # document 42 | end 43 | 44 | def each 45 | data = get_data 46 | data.each do |document| 47 | Jekyll.logger.debug "StrapiCollection iterating over document:" "#{collection_name} #{document.id}" 48 | document.type = collection_name 49 | document.collection = collection_name 50 | document.id ||= document._id 51 | if single_request? 52 | document.strapi_attributes = document.attributes 53 | else 54 | document_response = get_document(document.id) 55 | # We will keep all the attributes in strapi_attributes 56 | document.strapi_attributes = document_response['data']["attributes"] 57 | end 58 | document.url = @site.strapi_link_resolver(collection_name, document) 59 | end 60 | data.each {|x| yield(x)} 61 | end 62 | 63 | def endpoint 64 | @config['type'] || @collection_name 65 | end 66 | 67 | def permalink 68 | @permalink ||= StrapiCollectionPermalink.new(collection: self, lang: @site.lang) 69 | end 70 | 71 | def populate 72 | @config["populate"] || "*" 73 | end 74 | 75 | def path_params 76 | string = "?" 77 | return_params = false 78 | 79 | if @config["parameters"] 80 | return_params = true 81 | 82 | @config["parameters"].each do |k, v| 83 | string += "&#{k}=#{v}" 84 | end 85 | end 86 | 87 | if custom_path_params.length != 0 88 | return_params = true 89 | 90 | string += custom_path_params 91 | end 92 | 93 | return_params ? string : "" 94 | end 95 | 96 | def single_request? 97 | @config["single_request"] == true 98 | end 99 | 100 | def custom_path_params 101 | # Define custom logic in your _plugins/file_name.rb 102 | "" 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/jekyll/strapi4/collection_permalink.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module Strapi 3 | class StrapiCollectionPermalink 4 | attr_reader :collection, :lang 5 | 6 | def initialize(collection:, lang: nil) 7 | @collection = collection 8 | @lang = lang 9 | end 10 | 11 | def directory 12 | use_different? ? permalinks[lang].split("/")[1] : collection.collection_name 13 | end 14 | 15 | def exist? 16 | config.key? 'permalink' 17 | end 18 | 19 | def to_s 20 | use_different? ? permalinks[lang] : config['permalink'] 21 | end 22 | 23 | private 24 | 25 | def config 26 | collection.config 27 | end 28 | 29 | def permalinks 30 | config['permalinks'] 31 | end 32 | 33 | def use_different? 34 | permalinks && permalinks[lang] 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/jekyll/strapi4/drops.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module Strapi 3 | # Strapi Document access in Liquid 4 | class StrapiDocumentDrop < Liquid::Drop 5 | attr_accessor :document 6 | 7 | def initialize(document) 8 | @document = document 9 | end 10 | 11 | def [](attribute) 12 | value = @document[attribute] 13 | 14 | case value 15 | when OpenStruct 16 | StrapiDocumentDrop.new(value) 17 | when Array 18 | StrapiCollectionDrop.new(value) 19 | else 20 | value 21 | end 22 | end 23 | end 24 | 25 | # Handles a single Strapi collection in Liquid 26 | class StrapiCollectionDrop < Liquid::Drop 27 | def initialize(collection) 28 | @collection = collection 29 | end 30 | 31 | def to_liquid 32 | results = [] 33 | @collection.each do |result| 34 | results << StrapiDocumentDrop.new(result) 35 | end 36 | results 37 | end 38 | end 39 | 40 | # Handles Strapi collections in Liquid, and creates the collection on demand 41 | class StrapiCollectionsDrop < Liquid::Drop 42 | def initialize(collections) 43 | @collections = collections 44 | end 45 | 46 | def [](collection_name) 47 | StrapiCollectionDrop.new(@collections[collection_name]) 48 | end 49 | end 50 | 51 | # Main Liquid Drop, handles lazy loading of collections to drops 52 | class StrapiDrop < Liquid::Drop 53 | def initialize(site) 54 | @site = site 55 | end 56 | 57 | def collections 58 | @collections ||= StrapiCollectionsDrop.new(@site.strapi_collections) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/jekyll/strapi4/generator.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module Strapi 3 | # Generates pages for all collections with have the "generate" option set to True 4 | class StrapiGenerator < Generator 5 | safe true 6 | 7 | def generate(site) 8 | site.strapi_collections.each do |collection_name, collection| 9 | if collection.generate? 10 | collection.each do |document| 11 | site.pages << StrapiPage.new(site, site.source, document, collection) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | 18 | class StrapiPage < Page 19 | def initialize(site, base, document, collection) 20 | @site = site 21 | @base = base 22 | @collection = collection 23 | @document = document 24 | 25 | @site.lang = @document.attributes.locale 26 | 27 | @dir = @collection.config['output_dir'] || collection.permalink.directory 28 | 29 | url = Jekyll::URL.new( 30 | :placeholders => { 31 | :id => document.id.to_s, 32 | :uid => document.uid, 33 | :slug => document.attributes.slug, 34 | :type => document.attributes.type, 35 | :date => document.attributes.date, 36 | :title => document.title 37 | }, 38 | permalink: collection.permalink.to_s 39 | ) 40 | 41 | # Default file name, can be overwritten by permalink frontmatter setting 42 | file_name = url.to_s.split("/").last 43 | @name = "#{file_name}.html" 44 | # filename_to_read = File.join(base, "_layouts"), @collection.config['layout'] 45 | 46 | self.process(@name) 47 | self.read_yaml(File.join(base, "_layouts"), @collection.config['layout']) 48 | if @collection.permalink.exist? 49 | self.data['permalink'] = @collection.permalink.to_s 50 | end 51 | 52 | self.data['document'] = StrapiDocumentDrop.new(@document) 53 | end 54 | 55 | def url_placeholders 56 | # This was not really providing :id for the mapping 57 | # requiredValues = @document.strapi_attributes.to_h.select {|k, v| 58 | # v.class == String and @collection.config['permalink'].include? k.to_s 59 | # } 60 | my_hash = { 61 | :id => @document.id.to_s, 62 | :slug => @document.attributes.slug 63 | } 64 | Utils.deep_merge_hashes(my_hash, super) 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/jekyll/strapi4/hooks.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module Strapi 3 | # Add Strapi Liquid variables to all templates 4 | Jekyll::Hooks.register :site, :pre_render do |site, payload| 5 | payload['strapi'] = StrapiDrop.new(site) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/jekyll/strapi4/site.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | # Add helper methods for dealing with Strapi to the Site class 3 | class Site 4 | attr_accessor :lang 5 | 6 | def strapi 7 | return nil unless has_strapi? 8 | end 9 | 10 | def strapi_collections 11 | return Array.new unless has_strapi_collections? 12 | @strapi_collections ||= Hash[@config['strapi']['collections'].map {|name, config| [name, Strapi::StrapiCollection.new(self, name, config)]}] 13 | end 14 | 15 | def has_strapi? 16 | @config['strapi'] != nil 17 | end 18 | 19 | def has_strapi_collections? 20 | has_strapi? and @config['strapi']['collections'] != nil 21 | end 22 | 23 | def endpoint 24 | has_strapi? and @config['strapi']['endpoint'] or "http://localhost:1337" 25 | end 26 | 27 | def strapi_link_resolver(collection = nil, document = nil) 28 | return "/" unless collection != nil and @config['strapi']['collections'][collection]['permalink'] != nil 29 | url = Jekyll::URL.new( 30 | :template => url_template(collection), 31 | :placeholders => { 32 | :id => document.id.to_s, 33 | :uid => document.uid, 34 | :slug => document.attributes.slug, 35 | :type => document.attributes.type, 36 | :date => document.attributes.date, 37 | :title => document.title 38 | } 39 | ) 40 | 41 | url.to_s 42 | end 43 | 44 | def strapi_collection(collection_name) 45 | strapi_collections[collection_name] 46 | end 47 | 48 | def url_template(collection) 49 | permalink = @config['strapi']['collections'][collection]['permalink'] 50 | permalinks = @config['strapi']['collections'][collection]['permalinks'] 51 | 52 | return permalink unless permalinks && permalinks[lang] 53 | "/#{lang}#{permalinks[lang]}" 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/jekyll/strapi4/strapihttp.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # This is a helper method to authenticate during getting data from Strapi instance. 3 | require "json" 4 | 5 | def strapi_request(url) 6 | uri = URI(url) 7 | req = Net::HTTP::Get.new(uri) 8 | strapi_token = ENV['STRAPI_TOKEN'] 9 | if strapi_token==nil 10 | Jekyll.logger.info "STRAPI_TOKEN not set, non Authenticated request." 11 | headers = {} 12 | else 13 | headers = { 14 | 'Authorization'=>"Bearer #{strapi_token}" 15 | } 16 | req['Authorization'] = "Bearer #{strapi_token}" 17 | end 18 | Jekyll.logger.info "Jekyll StrapiHTTP:", "Fetching entries from #{uri} using headers: #{headers.keys}" 19 | response = Net::HTTP.get_response(uri, headers) 20 | 21 | ## 22 | # Response structure 23 | # https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest-api.html#unified-response-format 24 | # - data 25 | # - meta 26 | # - error 27 | # TODO: add checking error and the meta and act if necessart 28 | 29 | # Check response code 30 | if response.code == "200" 31 | response_json = JSON.parse(response.body, object_class: OpenStruct) 32 | return response_json 33 | elsif response.code == "401" 34 | raise "The Strapi server sent a error with the following status: 401. Please make sure that your credentials are correct or that you have access to the API." 35 | elsif response.code == "403" 36 | raise "The Strapi server sent a error with the following status: 403. Please provide STRAPI_TOKEN or allow public access for find and findOne actions." 37 | elsif response.code == "403" 38 | raise "The Strapi server sent a error with the following status: 404. Please make sure that name of your collection is correct." 39 | else 40 | raise "The Strapi server sent a error with the following status: #{response.code}. Please make sure it is correctly running." 41 | end 42 | end -------------------------------------------------------------------------------- /lib/jekyll/strapi4/version.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module Strapi 3 | VERSION = "1.0.12" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/jekyll/tags/strapiimagefilter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "down" 3 | 4 | module Jekyll 5 | class StrapiStaticFile < Jekyll::StaticFile 6 | def initialize(site, base, dir, name, collection = nil) 7 | @site = site 8 | @base = base 9 | @dir = dir 10 | @name = name 11 | @collection = collection 12 | # TODO: Check if user can change 'assets' path 13 | @relative_path = File.join(*["assets", @name].compact) 14 | @extname = File.extname(@name) 15 | end 16 | end 17 | 18 | module StrapiImageFilter 19 | def asset_url(input) 20 | # Sometimes to make this output visible in tests debug must be change for info 21 | # Need to find switch of logging level during rake test TODO: <=== 22 | Jekyll.logger.debug "StrapiImageFilter 000 input:" " ==> YES" 23 | Jekyll.logger.debug "StrapiImageFilter 111 input:" "#{input}" 24 | Jekyll.logger.debug "StrapiImageFilter 222 context REGISTERS:" "#{@context.registers}" 25 | strapi_endpoint = @context.registers[:site].config['strapi']['endpoint'] 26 | Jekyll.logger.debug "StrapiImageFilter strapi_endpoint:" "#{strapi_endpoint}" 27 | 28 | uri_path = "#{strapi_endpoint}#{input['url']}" 29 | if not Dir.exist?('_tmp_assets') 30 | # TODO: Check if there is not ability to overwrite from the _config 31 | Jekyll.logger.info "_tmp_assets directory does not exist, I am going to create one" 32 | Dir.mkdir '_tmp_assets' 33 | end 34 | ## 35 | # TODO: Investigate if there is a better way to download binaries 36 | # Check if we need authenticate to get medias 37 | Down.download(uri_path, destination: "_tmp_assets/#{input['name']}") 38 | ## 39 | # To perform copying of the assets in the cycle of Jenkins 40 | # https://jekyllrb.com/docs/rendering-process/ 41 | site = Jekyll.sites.first 42 | site.static_files << StrapiStaticFile.new(site, site.source, "_tmp_assets", "#{input['name']}") 43 | "/assets/#{input['name']}" 44 | end 45 | end 46 | end 47 | Liquid::Template.register_filter(Jekyll::StrapiImageFilter) 48 | -------------------------------------------------------------------------------- /test/_layouts/post.html: -------------------------------------------------------------------------------- 1 |

Post

2 | -------------------------------------------------------------------------------- /test/source/_data/photo.01.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": 1, 4 | "attributes": { 5 | "Title": "Fallen guardian angel", 6 | "createdAt": "2022-06-17T19:44:36.067Z", 7 | "updatedAt": "2022-06-17T19:44:59.709Z", 8 | "publishedAt": "2022-06-17T19:44:37.237Z", 9 | "Comment": "Found during the night walk in Costa Brava.", 10 | "Image": { 11 | "data": { 12 | "id": 2, 13 | "attributes": { 14 | "name": "NoRight.JPG", 15 | "alternativeText": "NoRight.JPG", 16 | "caption": "NoRight.JPG", 17 | "width": 6960, 18 | "height": 4640, 19 | "formats": { 20 | "thumbnail": { 21 | "name": "thumbnail_NoRight.JPG", 22 | "hash": "thumbnail_No_Right_ee695cb7c5", 23 | "ext": ".JPG", 24 | "mime": "image/jpeg", 25 | "path": null, 26 | "width": 234, 27 | "height": 156, 28 | "size": 12.54, 29 | "url": "/uploads/thumbnail_No_Right_ee695cb7c5.JPG" 30 | }, 31 | "large": { 32 | "name": "large_NoRight.JPG", 33 | "hash": "large_No_Right_ee695cb7c5", 34 | "ext": ".JPG", 35 | "mime": "image/jpeg", 36 | "path": null, 37 | "width": 1000, 38 | "height": 667, 39 | "size": 134.67, 40 | "url": "/uploads/large_No_Right_ee695cb7c5.JPG" 41 | }, 42 | "medium": { 43 | "name": "medium_NoRight.JPG", 44 | "hash": "medium_No_Right_ee695cb7c5", 45 | "ext": ".JPG", 46 | "mime": "image/jpeg", 47 | "path": null, 48 | "width": 750, 49 | "height": 500, 50 | "size": 82.83, 51 | "url": "/uploads/medium_No_Right_ee695cb7c5.JPG" 52 | }, 53 | "small": { 54 | "name": "small_NoRight.JPG", 55 | "hash": "small_No_Right_ee695cb7c5", 56 | "ext": ".JPG", 57 | "mime": "image/jpeg", 58 | "path": null, 59 | "width": 500, 60 | "height": 333, 61 | "size": 43.56, 62 | "url": "/uploads/small_No_Right_ee695cb7c5.JPG" 63 | } 64 | }, 65 | "hash": "No_Right_ee695cb7c5", 66 | "ext": ".JPG", 67 | "mime": "image/jpeg", 68 | "size": 3909.44, 69 | "url": "/uploads/No_Right_ee695cb7c5.JPG", 70 | "previewUrl": null, 71 | "provider": "local", 72 | "provider_metadata": null, 73 | "createdAt": "2022-06-17T19:43:36.273Z", 74 | "updatedAt": "2022-06-17T19:43:36.273Z" 75 | } 76 | } 77 | } 78 | } 79 | }, 80 | "meta": {} 81 | } -------------------------------------------------------------------------------- /test/source/_data/photos.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "id": 1, 5 | "attributes": { 6 | "Title": "Fallen guardian angel", 7 | "createdAt": "2022-06-17T19:44:36.067Z", 8 | "updatedAt": "2022-06-17T19:44:59.709Z", 9 | "publishedAt": "2022-06-17T19:44:37.237Z", 10 | "Comment": "Found during the night walk in Costa Brava." 11 | } 12 | } 13 | ], 14 | "meta": { 15 | "pagination": { 16 | "page": 1, 17 | "pageSize": 25, 18 | "pageCount": 1, 19 | "total": 1 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /test/test_collection.rb: -------------------------------------------------------------------------------- 1 | require 'jekyll' 2 | require 'jekyll/strapi4/strapihttp' 3 | require 'jekyll/strapi4/collection' 4 | require 'jekyll/strapi4/collection_permalink' 5 | require 'jekyll/strapi4/drops' 6 | require 'jekyll/tags/strapiimagefilter' 7 | require "test/unit" 8 | require 'jekyll/tags/strapiimagefilter' 9 | require 'liquid/template' 10 | 11 | ## 12 | # Okay, so there is a lot of DRY (Do Repeat Yourself) - but it is working and code 13 | # will improve in time - more modular. 14 | 15 | ## Helper methods 16 | def setup_collection 17 | @config_site = {"source"=>"#{Dir.getwd}", "destination"=>"#{Dir.getwd}/_site", "collections_dir"=>"", "cache_dir"=>".jekyll-cache", "plugins_dir"=>"_plugins", "layouts_dir"=>"_layouts", "data_dir"=>"_data", "includes_dir"=>"_includes", "collections"=>{"posts"=>{"output"=>true, "permalink"=>"/:categories/:year/:month/:day/:title:output_ext"}}, "safe"=>false, "include"=>[".htaccess"], "exclude"=>[".sass-cache", ".jekyll-cache", "gemfiles", "Gemfile", "Gemfile.lock", "node_modules", "vendor/bundle/", "vendor/cache/", "vendor/gems/", "vendor/ruby/"], "keep_files"=>[".git", ".svn"], "encoding"=>"utf-8", "markdown_ext"=>"markdown,mkdown,mkdn,mkd,md", "strict_front_matter"=>false, "show_drafts"=>nil, "limit_posts"=>0, "future"=>false, "unpublished"=>false, "whitelist"=>[], "plugins"=>["jekyll-feed", "jekyll-strapi-4"], "markdown"=>"kramdown", "highlighter"=>"rouge", "lsi"=>false, "excerpt_separator"=>"\n\n", "incremental"=>false, "detach"=>false, "port"=>"4000", "host"=>"127.0.0.1", "baseurl"=>"", "show_dir_listing"=>false, "permalink"=>"date", "paginate_path"=>"/page:num", "timezone"=>nil, "quiet"=>false, "verbose"=>true, "defaults"=>[], "liquid"=>{"error_mode"=>"warn", "strict_filters"=>false, "strict_variables"=>false}, "kramdown"=>{"auto_ids"=>true, "toc_levels"=>[1, 2, 3, 4, 5, 6], "entity_output"=>"as_char", "smart_quotes"=>"lsquo,rsquo,ldquo,rdquo", "input"=>"GFM", "hard_wrap"=>false, "guess_lang"=>true, "footnote_nr"=>1, "show_warnings"=>false}, "title"=>"Your awesome title", "description"=>"Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.", "url"=>"", "theme"=>"minima", "strapi"=>{"endpoint"=>"http://localhost:1337", "collections"=>{"posts"=>{"permalink"=>"/blog/:slug/", "layout"=>"post.html", "output"=>true}}}, "serving"=>false} 18 | @site = Jekyll::Site.new(@config_site) 19 | @collection_name = "posts" 20 | end 21 | 22 | ## 23 | # Monkey patching of the Down module to allow 24 | # smooth execution of the filter during the UnitTests 25 | 26 | module Down 27 | module_function 28 | 29 | class Down 30 | VERSION = 0 31 | class ConnectionError 32 | end 33 | end 34 | 35 | def download(*args, **options, &block) 36 | Jekyll.logger.info "STRAPI TESTS:" "MonkeyPatch Down::download" 37 | end 38 | 39 | def open(*args, **options, &block) 40 | Jekyll.logger.info "STRAPI TESTS:" "MonkeyPatch Down::open" 41 | end 42 | 43 | def backend(value = nil) 44 | Jekyll.logger.info "STRAPI TESTS:" "MonkeyPatch Down::backend" 45 | end 46 | end 47 | 48 | ## 49 | # Some 'mocks', likely to be rewritten 50 | 51 | module Jekyll 52 | module Strapi 53 | class StrapiCollectionMock 54 | # attr_accessor :collection_name, :config 55 | 56 | def initialize 57 | @config_site = {"source"=>"/tmp/jekyll-strapi-src", "destination"=>"/tmp/jekyll-strapi-src/_site", "collections_dir"=>"", "cache_dir"=>".jekyll-cache", "plugins_dir"=>"_plugins", "layouts_dir"=>"_layouts", "data_dir"=>"_data", "includes_dir"=>"_includes", "collections"=>{"posts"=>{"output"=>true, "permalink"=>"/:categories/:year/:month/:day/:title:output_ext"}}, "safe"=>false, "include"=>[".htaccess"], "exclude"=>[".sass-cache", ".jekyll-cache", "gemfiles", "Gemfile", "Gemfile.lock", "node_modules", "vendor/bundle/", "vendor/cache/", "vendor/gems/", "vendor/ruby/"], "keep_files"=>[".git", ".svn"], "encoding"=>"utf-8", "markdown_ext"=>"markdown,mkdown,mkdn,mkd,md", "strict_front_matter"=>false, "show_drafts"=>nil, "limit_posts"=>0, "future"=>false, "unpublished"=>false, "whitelist"=>[], "plugins"=>["jekyll-feed", "jekyll-strapi-4"], "markdown"=>"kramdown", "highlighter"=>"rouge", "lsi"=>false, "excerpt_separator"=>"\n\n", "incremental"=>false, "detach"=>false, "port"=>"4000", "host"=>"127.0.0.1", "baseurl"=>"", "show_dir_listing"=>false, "permalink"=>"date", "paginate_path"=>"/page:num", "timezone"=>nil, "quiet"=>false, "verbose"=>true, "defaults"=>[], "liquid"=>{"error_mode"=>"warn", "strict_filters"=>false, "strict_variables"=>false}, "kramdown"=>{"auto_ids"=>true, "toc_levels"=>[1, 2, 3, 4, 5, 6], "entity_output"=>"as_char", "smart_quotes"=>"lsquo,rsquo,ldquo,rdquo", "input"=>"GFM", "hard_wrap"=>false, "guess_lang"=>true, "footnote_nr"=>1, "show_warnings"=>false}, "title"=>"Your awesome title", "description"=>"Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.", "url"=>"", "theme"=>"minima", "strapi"=>{"endpoint"=>"http://localhost:1337", "collections"=>{"photos"=>{"permalink"=>"/photos/:id/", "layout"=>"photo.html", "output"=>true}}}, "serving"=>false} 58 | @collection_name = "photos" 59 | @config = '{"permalink"=>"/photos/:id/", "layout"=>"photo.html", "output"=>true}' 60 | @site = Jekyll::Site.new(@config_site) 61 | 62 | Jekyll.logger.info "Jekyll MOCK Collection init:" "#{@site} #{@collection_name} #{@config}" 63 | end 64 | 65 | def generate? 66 | @config['output'] || false 67 | end 68 | 69 | def get_data 70 | file = File.read('test/source/_data/photos.json') 71 | response = JSON.parse(file, object_class: OpenStruct) 72 | Jekyll.logger.info "STRAPI TEST RESPONSE:" "#{response}" 73 | response.data 74 | end 75 | 76 | def get_document(did) 77 | file = File.read('test/source/_data/photo.01.json') 78 | response = JSON.parse(file, object_class: OpenStruct) 79 | Jekyll.logger.debug "StrapiCollection GET_DOCUMENT:" "#{response} #{did}" 80 | response 81 | end 82 | 83 | def each 84 | data = get_data 85 | data.each do |document| 86 | ## 87 | # This should matach what is inside collection.rb 88 | # TODO: make it modular co same code can be reused 89 | Jekyll.logger.debug "StrapiCollection iterating over document:" "#{@collection_name} #{document.id}" 90 | document.type = @collection_name 91 | document.collection = @collection_name 92 | document.id ||= document._id 93 | document_response = get_document(document.id) 94 | # We will keep all the attributes in strapi_attributes 95 | document.strapi_attributes = document_response['data']["attributes"] 96 | Jekyll.logger.info "STRAPI COLLECTION MOCK:" "#{document}" 97 | document.url = @site.strapi_link_resolver(@collection_name, document) 98 | end 99 | data.each {|x| yield(x)} 100 | end 101 | end 102 | end 103 | end 104 | 105 | ## 106 | # This code is working, but is very messy. It is late, 107 | # so I will clean up the other day. But first prof of concept 108 | # of the working and useful unittest is here! 109 | 110 | class TestCreateStrapiCollection < Test::Unit::TestCase 111 | def setup 112 | @collection = Jekyll::Strapi::StrapiCollectionMock.new() 113 | @my_array = @collection.each{|i|} 114 | @document = @my_array[0] 115 | Jekyll.logger.info "STRAPI-Jekyll TEST document:" "#{@document}" 116 | @drop = Jekyll::Strapi::StrapiDocumentDrop.new(@document) 117 | Jekyll.logger.info "STRAPI-Jekyll TEST drop:" "#{@drop}" 118 | 119 | @filter = Object.new.extend(Jekyll::StrapiImageFilter) 120 | @template = Liquid::Template.parse("{{ document.strapi_attributes.Image.data.attributes.formats.thumbnail |asset_url}}") 121 | # @template.render!(@info, {registers:{:site=>"a", :b=>"aa", :page=>{"document"=>@drop}, :config=>{}}}) 122 | @context = Liquid::Context.new() 123 | @context['document'] = @drop 124 | # a = @filter.asset_url(@drop) 125 | end 126 | 127 | def test_create 128 | 129 | @config_site = {"source"=>"/tmp/jekyll-strapi-src", "destination"=>"/tmp/jekyll-strapi-src/_site", "collections_dir"=>"", "cache_dir"=>".jekyll-cache", "plugins_dir"=>"_plugins", "layouts_dir"=>"_layouts", "data_dir"=>"_data", "includes_dir"=>"_includes", "collections"=>{"posts"=>{"output"=>true, "permalink"=>"/:categories/:year/:month/:day/:title:output_ext"}}, "safe"=>false, "include"=>[".htaccess"], "exclude"=>[".sass-cache", ".jekyll-cache", "gemfiles", "Gemfile", "Gemfile.lock", "node_modules", "vendor/bundle/", "vendor/cache/", "vendor/gems/", "vendor/ruby/"], "keep_files"=>[".git", ".svn"], "encoding"=>"utf-8", "markdown_ext"=>"markdown,mkdown,mkdn,mkd,md", "strict_front_matter"=>false, "show_drafts"=>nil, "limit_posts"=>0, "future"=>false, "unpublished"=>false, "whitelist"=>[], "plugins"=>["jekyll-feed", "jekyll-strapi-4"], "markdown"=>"kramdown", "highlighter"=>"rouge", "lsi"=>false, "excerpt_separator"=>"\n\n", "incremental"=>false, "detach"=>false, "port"=>"4000", "host"=>"127.0.0.1", "baseurl"=>"", "show_dir_listing"=>false, "permalink"=>"date", "paginate_path"=>"/page:num", "timezone"=>nil, "quiet"=>false, "verbose"=>true, "defaults"=>[], "liquid"=>{"error_mode"=>"warn", "strict_filters"=>false, "strict_variables"=>false}, "kramdown"=>{"auto_ids"=>true, "toc_levels"=>[1, 2, 3, 4, 5, 6], "entity_output"=>"as_char", "smart_quotes"=>"lsquo,rsquo,ldquo,rdquo", "input"=>"GFM", "hard_wrap"=>false, "guess_lang"=>true, "footnote_nr"=>1, "show_warnings"=>false}, "title"=>"Your awesome title", "description"=>"Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.", "url"=>"", "theme"=>"minima", "strapi"=>{"endpoint"=>"http://localhost:1337", "collections"=>{"photos"=>{"permalink"=>"/photos/:id/", "layout"=>"photo.html", "output"=>true}}}, "serving"=>false} 130 | # @config = '{"permalink"=>"/photos/:id/", "layout"=>"photo.html", "output"=>true}' 131 | @site = Jekyll::Site.new(@config_site) 132 | # @template.render!(@context, {registers:{:site=>"v", :b=>"aa", :page=>{"document"=>@drop}, :config=>{}}} 133 | a = @template.render!({"document"=>@drop}, {registers:{:site=>@site, :b=>"aa", :page=>{"document"=>@drop}, :config=>{}}}) 134 | # @template.render!({}, registers={:site=>"v", :b=>"aa", :page=>{"document"=>@drop}, :config=>{}}) 135 | 136 | assert_equal(a, "/assets/thumbnail_NoRight.JPG") 137 | end 138 | end 139 | 140 | class TestStrapiCollectionEndpoint < Test::Unit::TestCase 141 | def setup 142 | setup_collection 143 | end 144 | 145 | def test_default 146 | @config = {"permalink"=>"/blog/:slug/", "layout"=>"post.html", "output"=>true} 147 | @collection = Jekyll::Strapi::StrapiCollection.new(@site, @collection_name, @config) 148 | 149 | assert_equal("posts", @collection.endpoint) 150 | end 151 | 152 | def test_given 153 | @config = {"permalink"=>"/blog/:slug/", "layout"=>"post.html", "output"=>true, "type"=>"other_posts"} 154 | @collection = Jekyll::Strapi::StrapiCollection.new(@site, @collection_name, @config) 155 | 156 | assert_equal("other_posts", @collection.endpoint) 157 | end 158 | end 159 | 160 | class TestStrapiCollectionPopulate < Test::Unit::TestCase 161 | def setup 162 | setup_collection 163 | end 164 | 165 | def test_default 166 | @config = {"permalink"=>"/blog/:slug/", "layout"=>"post.html", "output"=>true} 167 | @collection = Jekyll::Strapi::StrapiCollection.new(@site, @collection_name, @config) 168 | 169 | assert_equal("*", @collection.populate) 170 | end 171 | 172 | def test_given 173 | @config = {"permalink"=>"/blog/:slug/", "layout"=>"post.html", "output"=>true, "populate"=>"deep"} 174 | @collection = Jekyll::Strapi::StrapiCollection.new(@site, @collection_name, @config) 175 | 176 | assert_equal("deep", @collection.populate) 177 | end 178 | end 179 | 180 | class TestStrapiCollectionSingleRequest < Test::Unit::TestCase 181 | def setup 182 | setup_collection 183 | end 184 | 185 | def test_default 186 | @config = {"permalink"=>"/blog/:slug/", "layout"=>"post.html", "output"=>true} 187 | @collection = Jekyll::Strapi::StrapiCollection.new(@site, @collection_name, @config) 188 | 189 | assert_false @collection.single_request? 190 | end 191 | 192 | def test_given_false 193 | @config = {"permalink"=>"/blog/:slug/", "layout"=>"post.html", "output"=>true, "single_request"=>false} 194 | @collection = Jekyll::Strapi::StrapiCollection.new(@site, @collection_name, @config) 195 | 196 | assert_false @collection.single_request? 197 | end 198 | 199 | def test_given_true 200 | @config = {"permalink"=>"/blog/:slug/", "layout"=>"post.html", "output"=>true, "single_request"=>true} 201 | @collection = Jekyll::Strapi::StrapiCollection.new(@site, @collection_name, @config) 202 | 203 | assert_true @collection.single_request? 204 | end 205 | end 206 | 207 | class TestStrapiCollectionPathParams < Test::Unit::TestCase 208 | def setup 209 | setup_collection 210 | end 211 | 212 | def test_default 213 | @config = {"permalink"=>"/blog/:slug/", "layout"=>"post.html", "output"=>true} 214 | @collection = Jekyll::Strapi::StrapiCollection.new(@site, @collection_name, @config) 215 | 216 | assert_equal("", @collection.path_params) 217 | end 218 | 219 | def test_given 220 | @config = {"permalink"=>"/blog/:slug/", "layout"=>"post.html", "output"=>true, "parameters"=>{"sort"=>"publicationDate:desc"}} 221 | @collection = Jekyll::Strapi::StrapiCollection.new(@site, @collection_name, @config) 222 | 223 | assert_equal("?&sort=publicationDate:desc", @collection.path_params) 224 | end 225 | end 226 | 227 | class TestStrapiCollectionPermalinkParams < Test::Unit::TestCase 228 | def setup 229 | setup_collection 230 | 231 | config = {"permalink"=>"/blog/:slug/", "permalinks"=>{"pl"=>"/poradnik/:slug/"}, "layout"=>"post.html", "output"=>true} 232 | collection = Jekyll::Strapi::StrapiCollection.new(@site, @collection_name, config) 233 | 234 | no_permalink_config = {"layout"=>"post.html", "output"=>true} 235 | no_permalink_collection = Jekyll::Strapi::StrapiCollection.new(@site, @collection_name, no_permalink_config) 236 | 237 | @permalink = Jekyll::Strapi::StrapiCollectionPermalink.new(collection: collection) 238 | @permalink_pl = Jekyll::Strapi::StrapiCollectionPermalink.new(collection: collection, lang: "pl") 239 | @no_permalink = Jekyll::Strapi::StrapiCollectionPermalink.new(collection: no_permalink_collection) 240 | end 241 | 242 | def test_directory 243 | assert_equal("posts", @permalink.directory) 244 | assert_equal("poradnik", @permalink_pl.directory) 245 | assert_equal("posts", @no_permalink.directory) 246 | end 247 | 248 | def test_exist? 249 | assert @permalink.exist? 250 | assert @permalink_pl.exist? 251 | assert_false @no_permalink.exist? 252 | end 253 | 254 | def test_to_s 255 | assert_equal("/blog/:slug/", @permalink.to_s) 256 | assert_equal("/poradnik/:slug/", @permalink_pl.to_s) 257 | assert_nil(@no_permalink.to_s) 258 | end 259 | end 260 | -------------------------------------------------------------------------------- /test/test_hello.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Example of the UnitTest. More can be found here: 3 | # https://en.wikibooks.org/wiki/Ruby_Programming/Unit_testing 4 | 5 | require "test/unit" 6 | 7 | class TestSimpleHello < Test::Unit::TestCase 8 | def setup 9 | # SetUp 10 | end 11 | 12 | def teardown 13 | # TearDown 14 | end 15 | 16 | def test_rake 17 | assert_equal(4, 4) 18 | assert_equal(6, 6) 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /test/test_strapi_page.rb: -------------------------------------------------------------------------------- 1 | require 'jekyll' 2 | require "test/unit" 3 | 4 | ## Helper methods 5 | def setup_page 6 | # Duplicate with setup_collection helper method 7 | @config_site = {"source"=>"#{Dir.getwd}", "destination"=>"#{Dir.getwd}/_site", "collections_dir"=>"", "cache_dir"=>".jekyll-cache", "plugins_dir"=>"_plugins", "layouts_dir"=>"_layouts", "data_dir"=>"_data", "includes_dir"=>"_includes", "collections"=>{"posts"=>{"output"=>true, "permalink"=>"/:categories/:year/:month/:day/:title:output_ext"}}, "safe"=>false, "include"=>[".htaccess"], "exclude"=>[".sass-cache", ".jekyll-cache", "gemfiles", "Gemfile", "Gemfile.lock", "node_modules", "vendor/bundle/", "vendor/cache/", "vendor/gems/", "vendor/ruby/"], "keep_files"=>[".git", ".svn"], "encoding"=>"utf-8", "markdown_ext"=>"markdown,mkdown,mkdn,mkd,md", "strict_front_matter"=>false, "show_drafts"=>nil, "limit_posts"=>0, "future"=>false, "unpublished"=>false, "whitelist"=>[], "plugins"=>["jekyll-feed", "jekyll-strapi-4"], "markdown"=>"kramdown", "highlighter"=>"rouge", "lsi"=>false, "excerpt_separator"=>"\n\n", "incremental"=>false, "detach"=>false, "port"=>"4000", "host"=>"127.0.0.1", "baseurl"=>"", "show_dir_listing"=>false, "permalink"=>"date", "paginate_path"=>"/page:num", "timezone"=>nil, "quiet"=>false, "verbose"=>true, "defaults"=>[], "liquid"=>{"error_mode"=>"warn", "strict_filters"=>false, "strict_variables"=>false}, "kramdown"=>{"auto_ids"=>true, "toc_levels"=>[1, 2, 3, 4, 5, 6], "entity_output"=>"as_char", "smart_quotes"=>"lsquo,rsquo,ldquo,rdquo", "input"=>"GFM", "hard_wrap"=>false, "guess_lang"=>true, "footnote_nr"=>1, "show_warnings"=>false}, "title"=>"Your awesome title", "description"=>"Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.", "url"=>"", "theme"=>"minima", "strapi"=>{"endpoint"=>"http://localhost:1337", "collections"=>{"posts"=>{"permalink"=>"/blog/:slug/", "layout"=>"post.html", "output"=>true}}}, "serving"=>false} 8 | @site = Jekyll::Site.new(@config_site) 9 | @collection_name = "posts" 10 | 11 | @config = {"permalink"=>"/blog/:slug/", "layout"=>"post.html", "output"=>true} 12 | @collection = Jekyll::Strapi::StrapiCollection.new(@site, @collection_name, @config) 13 | 14 | @base = "/test" 15 | @document = OpenStruct.new( 16 | id: "1", 17 | attributes: OpenStruct.new(slug: "first-post") 18 | ) 19 | 20 | @strapi_page = Jekyll::Strapi::StrapiPage.new(@site, @base, @document, @collection) 21 | end 22 | 23 | class TestStrapiPageUrlPlaceholder < Test::Unit::TestCase 24 | def setup 25 | setup_page 26 | end 27 | 28 | def test_url_placeholder 29 | assert_equal("1", @strapi_page.url_placeholders[:id]) 30 | assert_equal("first-post", @strapi_page.url_placeholders[:slug]) 31 | assert_equal(nil, @strapi_page.url_placeholders[:other]) 32 | end 33 | end 34 | --------------------------------------------------------------------------------