├── .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 | 
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 | 
45 |
46 | To creat the fields choose:
47 |
48 | **Text** field for the *Title*, type of **Short text**:
49 |
50 |
51 | 
52 |
53 | **Media** field with a name *Image*, type **Single media**:
54 |
55 | 
56 |
57 | **Text** *Comment* field, we will use **Long text**:
58 |
59 | 
60 |
61 | Finally you should have something similar to:
62 |
63 | 
64 |
65 |
66 | Now go to **Content Manager** and add your first object to the database:
67 |
68 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------