contentful_middleman 0.0.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -7
- data/README.md +118 -44
- data/Rakefile +1 -1
- data/contentful_middleman.gemspec +2 -2
- data/lib/contentful_middleman.rb +1 -1
- data/lib/contentful_middleman/commands/contentful.rb +79 -0
- data/lib/contentful_middleman/commands/context.rb +76 -0
- data/lib/contentful_middleman/core.rb +45 -43
- data/lib/contentful_middleman/helpers.rb +7 -0
- data/lib/contentful_middleman/import_task.rb +42 -0
- data/lib/contentful_middleman/instance.rb +45 -0
- data/lib/contentful_middleman/local_data/file.rb +29 -0
- data/lib/contentful_middleman/local_data/store.rb +37 -0
- data/lib/contentful_middleman/mappers/base.rb +39 -0
- data/lib/contentful_middleman/tools/backup.rb +87 -0
- data/lib/contentful_middleman/version.rb +1 -1
- data/lib/contentful_middleman/version_hash.rb +29 -0
- data/spec/contentful_middleman_spec.rb +1 -3
- metadata +26 -16
- data/lib/contentful_middleman/commands/article.tt +0 -9
- data/lib/contentful_middleman/commands/sync_blog.rb +0 -88
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e5976bd4bf0b3954a33f737d13b8d9e2b928a9f8
|
4
|
+
data.tar.gz: 27378bfcc1245e25474fa1ccb26ccdbee82ba203
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 687bf3399611ae1cabf559862d375914afc69fdb93df7bcad0258875b5b42c46cb6f8a3e7540639f464ec71d892df862ea6d9f0dd246c41cab0a46f79d65d04c
|
7
|
+
data.tar.gz: 007f63c5d2fb64088ef48590e17eb5f1539afcb6ff1eeaf85d6f7fcb758a0ab470216ba61211046943cac18aa24ad25c22c6f992c4281a94a6467782fa568d94
|
data/CHANGELOG.md
CHANGED
@@ -1,15 +1,28 @@
|
|
1
|
-
|
2
|
-
===
|
1
|
+
# Change Log
|
3
2
|
|
3
|
+
## 1.0.
|
4
|
+
### Other
|
5
|
+
This release brings breaking changes that are not compatible with extension configurations in
|
6
|
+
previous versions. For more information about the supported configuration please read the
|
7
|
+
README file.
|
8
|
+
|
9
|
+
Changes in this release:
|
10
|
+
|
11
|
+
* Support multiple activations of the extension. Import from multiple spaces
|
12
|
+
* Decouple mapping of entries from blog post layout. Support custom mappers
|
13
|
+
* Store imported entries as local data
|
14
|
+
* Optionally rebuild static site only when there are changes in the imported data
|
15
|
+
|
16
|
+
## 0.0.4
|
17
|
+
### Other
|
4
18
|
* Publish first Gem version
|
5
19
|
|
6
|
-
0.0.3
|
7
|
-
===
|
20
|
+
## 0.0.3
|
8
21
|
|
22
|
+
### Other
|
9
23
|
* Minor updates
|
10
24
|
|
11
25
|
|
12
|
-
0.0.2
|
13
|
-
|
14
|
-
|
26
|
+
## 0.0.2
|
27
|
+
### Other
|
15
28
|
* First release
|
data/README.md
CHANGED
@@ -2,12 +2,10 @@
|
|
2
2
|
|
3
3
|
[](https://travis-ci.org/contentful/contentful_middleman)
|
4
4
|
|
5
|
-
Contentful Middleman is a [Middleman](http://middlemanapp.com/) extension to use the Middleman static
|
5
|
+
Contentful Middleman is a [Middleman](http://middlemanapp.com/) extension to use the Middleman static site generator together with the API-driven [Contentful CMS](https://www.contentful.com). It is powered by the [Contentful Ruby Gem](https://github.com/contentful/contentful.rb).
|
6
6
|
|
7
7
|
Experience the power of Middleman while staying sane as a developer by letting end-users edit content in a web-based interface.
|
8
8
|
|
9
|
-
This extensions supports both page-based content as well as blog posts through middleman-blog.
|
10
|
-
|
11
9
|
## Installation
|
12
10
|
|
13
11
|
Add the following line to the Gemfile of your Middleman project:
|
@@ -22,62 +20,138 @@ Then as usual, run:
|
|
22
20
|
bundle install
|
23
21
|
```
|
24
22
|
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Run `middleman contentful` in your terminal. This will fetch entries for the configured
|
26
|
+
spaces and content types and put the resulting data in the
|
27
|
+
[local data folder](https://middlemanapp.com/advanced/local-data/) as yaml files.
|
28
|
+
|
29
|
+
### --rebuild option
|
30
|
+
|
31
|
+
The `contentful` command has a `--rebuild` option which will trigger a rebuild of your site only if there were changes between the last
|
32
|
+
and the current import.
|
33
|
+
|
25
34
|
## Configuration
|
26
35
|
|
27
36
|
To configure the extension, add the following configuration block to Middleman's config.rb:
|
28
37
|
|
38
|
+
```ruby
|
39
|
+
activate :contentful do |f|
|
40
|
+
f.space = SPACE
|
41
|
+
f.access_token = ACCESS_TOKEN
|
42
|
+
f.cda_query = QUERY
|
43
|
+
f.content_types = CONTENT_TYPES_MAPPINGS
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
Parameter | Description
|
48
|
+
---------- |------------
|
49
|
+
space | Hash with an user choosen name for the space as key and the space id as value
|
50
|
+
access_token | Contentful Delivery API access token
|
51
|
+
cda_query | Hash describing query configuration. See [contentful.rb](https://github.com/contentful/contentful.rb) for more info
|
52
|
+
content_types | Hash describing the mapping applied to entries of the imported content types
|
53
|
+
|
54
|
+
You can activate the extension multiple times to import entries from different spaces.
|
55
|
+
## Entry mapping
|
56
|
+
|
57
|
+
The extension will transform every fetched entry before storing it as a yaml file in the local
|
58
|
+
data folder. If a custom mapper is not specified a default one will be used.
|
59
|
+
|
60
|
+
The default mapper will map fields, assets and linked entries.
|
61
|
+
|
62
|
+
### Custom mappers
|
63
|
+
|
64
|
+
You can create your own mappers if you need so. The only requirement for a class to behave as a
|
65
|
+
mapper is to have a `map(context, entry)` instance method. This method will take as parameters:
|
66
|
+
|
67
|
+
* A context object. All properties set on this object will be written to the yaml file
|
68
|
+
* An entry
|
69
|
+
|
70
|
+
Following is an example of such custom mapper:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
class MyAwesomeMapper
|
74
|
+
def map(context, entry)
|
75
|
+
context.slug = entry.title.parameterize
|
76
|
+
#... more transformations
|
77
|
+
end
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
If you don't want to map all the fields by hand inherit from the Base mappper:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
class MyAwesomeMapper < ContentfulMiddleman::Mappers::Base
|
85
|
+
def map(context, entry)
|
86
|
+
super
|
87
|
+
# After calling super the context object
|
88
|
+
# will have a property for every field in the
|
89
|
+
# entry
|
90
|
+
end
|
91
|
+
end
|
29
92
|
```
|
93
|
+
|
94
|
+
## Configuration: examples
|
95
|
+
|
96
|
+
```ruby
|
30
97
|
activate :contentful do |f|
|
31
|
-
|
32
|
-
f.
|
33
|
-
|
34
|
-
|
35
|
-
f.access_token = 'YOUR_CONTENT_DELIVERY_API_ACCESS_TOKEN'
|
36
|
-
|
37
|
-
# Optional: Options for middleman-blog
|
38
|
-
|
39
|
-
# Filter Entries for your blog posts. See Contentful gem and Content Delivery API documentation.
|
40
|
-
f.blog_posts_query = {content_type: "6LbnqgnwA08qYaU", category: "news" }
|
41
|
-
|
42
|
-
# Which keys to use in the article template for blog posts
|
43
|
-
# Key: template variable
|
44
|
-
# Value: Entry method or block
|
45
|
-
f.blog_post_mappings = {
|
46
|
-
slug: :id,
|
47
|
-
date: :created_at,
|
48
|
-
body: :id,
|
49
|
-
tags: :tags,
|
50
|
-
title: ->(e){"#{e.id}XXXX"}
|
51
|
-
}
|
52
|
-
|
53
|
-
# Define your own template for blog posts
|
54
|
-
f.new_article_template = "/my_templates/article.tt"
|
55
|
-
|
56
|
-
# Automatically synchronize blog posts before building with "middleman build"
|
57
|
-
f.sync_blog_before_build = true # default: false
|
98
|
+
f.space = {partners: 'space-id'}
|
99
|
+
f.access_token = 'some_access_token'
|
100
|
+
f.cda_query = { content_type: 'content-type-id', include: 1 }
|
101
|
+
f.content_types = { partner: 'content-type-id'}
|
58
102
|
end
|
59
103
|
```
|
104
|
+
The above configuration does the following:
|
60
105
|
|
61
|
-
|
106
|
+
* Sets the alias `partners` to the space with id _some-id_
|
107
|
+
* Sets the alias `partner` to the content type with id _content-type-id_
|
108
|
+
* Uses the default mapper to transform `partner` entries into yaml files (no mapper specified for the `partner` content type)
|
62
109
|
|
63
|
-
|
110
|
+
Entries fetched using this configuration will be stored as yaml files in `data/partners/partner/ENTRY_ID.yaml`.
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
class Mapper
|
114
|
+
def map(context, entry)
|
115
|
+
context.title = "#{entry.title}-title"
|
116
|
+
#...
|
117
|
+
end
|
118
|
+
end
|
64
119
|
|
120
|
+
activate :contentful do |f|
|
121
|
+
f.space = {partners: 'space-id'}
|
122
|
+
f.access_token = 'some_access_token'
|
123
|
+
f.cda_query = { content_type: '1EVL9Bl48Euu28QEOa44ai', include: 1 }
|
124
|
+
f.content_types = { partner: {mapper: Mapper, id: 'content-type-id'}}
|
125
|
+
end
|
65
126
|
```
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
127
|
+
|
128
|
+
The above configuration is the same as the previous one only that this time we are setting a custom mapper
|
129
|
+
for the entries belonging to the `partner` content type.
|
130
|
+
|
131
|
+
|
132
|
+
## Using imported entries in templates
|
133
|
+
|
134
|
+
Middleman will load all the yaml files stored in the local data folder. This lets you use all the imported
|
135
|
+
data into your templates.
|
136
|
+
|
137
|
+
Consider that we have data stored under `data/partners/partner`. Then in our templates we could use that data like
|
138
|
+
this:
|
139
|
+
|
140
|
+
```html
|
141
|
+
<h1>Partners</h1>
|
142
|
+
<ol>
|
143
|
+
<% data.partners.partner.each do |id, partner| %>
|
144
|
+
<li><%= partner["name"] %></li>
|
145
|
+
<% end %>
|
146
|
+
</ol>
|
75
147
|
```
|
76
148
|
|
77
|
-
|
149
|
+
### Rendering Markdown:
|
78
150
|
|
79
|
-
|
151
|
+
If you want to use markdown in your content types you manually have to render this to markdown.
|
152
|
+
Depending on the markdown library you need to transform the data.
|
153
|
+
For Kramdown this would be:
|
80
154
|
|
81
155
|
```
|
82
|
-
|
156
|
+
<%= Kramdown::Document.new(data).to_html %>
|
83
157
|
```
|
data/Rakefile
CHANGED
@@ -6,8 +6,8 @@ Gem::Specification.new do |s|
|
|
6
6
|
s.name = "contentful_middleman"
|
7
7
|
s.version = ContentfulMiddleman::VERSION
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
|
-
s.authors = ["Sascha Konietzke"]
|
10
|
-
s.email = ["sascha@contentful.com"]
|
9
|
+
s.authors = ["Sascha Konietzke", "Farruco Sanjurjoj"]
|
10
|
+
s.email = ["sascha@contentful.com", "madtrick@gmail.com"]
|
11
11
|
s.homepage = "https://www.contentful.com"
|
12
12
|
s.summary = %q{Include mangablable content from the Contentful CMS and API into your Middleman projects}
|
13
13
|
s.description = %q{Load blog posts and other managed content into Middleman}
|
data/lib/contentful_middleman.rb
CHANGED
@@ -2,6 +2,6 @@ require "middleman-core"
|
|
2
2
|
|
3
3
|
require 'contentful_middleman/version'
|
4
4
|
require 'contentful_middleman/core'
|
5
|
-
require "contentful_middleman/commands/
|
5
|
+
require "contentful_middleman/commands/contentful"
|
6
6
|
|
7
7
|
::Middleman::Extensions.register(:contentful, ContentfulMiddleman::Core)
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'middleman-core/cli'
|
2
|
+
require 'middleman-blog/uri_templates'
|
3
|
+
require 'date'
|
4
|
+
require 'digest'
|
5
|
+
require 'contentful_middleman/commands/context'
|
6
|
+
require 'contentful_middleman/tools/backup'
|
7
|
+
require 'contentful_middleman/version_hash'
|
8
|
+
require 'Contentful_middleman/import_task'
|
9
|
+
require 'contentful_middleman/local_data/store'
|
10
|
+
require 'contentful_middleman/local_data/file'
|
11
|
+
|
12
|
+
module Middleman
|
13
|
+
module Cli
|
14
|
+
# This class provides an "contentful" command for the middleman CLI.
|
15
|
+
|
16
|
+
class Contentful < Thor
|
17
|
+
include Thor::Actions
|
18
|
+
|
19
|
+
# Path where Middleman expects the local data to be stored
|
20
|
+
MIDDLEMAN_LOCAL_DATA_FOLDER = 'data'
|
21
|
+
|
22
|
+
check_unknown_options!
|
23
|
+
|
24
|
+
namespace :contentful
|
25
|
+
desc 'contentful', 'Import data from Contentful'
|
26
|
+
|
27
|
+
method_option "rebuild",
|
28
|
+
aliases: "-r",
|
29
|
+
desc: "Rebuilds the site if there were changes in the imported data"
|
30
|
+
|
31
|
+
def self.source_root
|
32
|
+
ENV['MM_ROOT']
|
33
|
+
end
|
34
|
+
|
35
|
+
# Tell Thor to exit with a nonzero exit code on failure
|
36
|
+
def self.exit_on_failure?
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
def contentful
|
41
|
+
if shared_instance.respond_to? :contentful_instances
|
42
|
+
ContentfulMiddleman::VersionHash.source_root = self.class.source_root
|
43
|
+
ContentfulMiddleman::LocalData::Store.base_path = MIDDLEMAN_LOCAL_DATA_FOLDER
|
44
|
+
ContentfulMiddleman::LocalData::File.thor = self
|
45
|
+
|
46
|
+
hash_local_data_changed = contentful_instances.reduce(false) do |changes, instance|
|
47
|
+
import_task = create_import_task(instance)
|
48
|
+
import_task.run
|
49
|
+
|
50
|
+
changes || import_task.changed_local_data?
|
51
|
+
end
|
52
|
+
|
53
|
+
Middleman::Cli::Build.new.build if hash_local_data_changed && options[:rebuild]
|
54
|
+
shared_instance.logger.info 'Contentful Import: Done!'
|
55
|
+
else
|
56
|
+
raise Thor::Error.new "You need to activate the contentful extension in config.rb before you can import data from Contentful"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def contentful_instances
|
62
|
+
shared_instance.contentful_instances
|
63
|
+
end
|
64
|
+
|
65
|
+
def create_import_task(instance)
|
66
|
+
space_name = instance.space_name.to_s
|
67
|
+
content_type_names = instance.content_types_ids_to_names
|
68
|
+
content_type_mappers = instance.content_types_ids_to_mappers
|
69
|
+
|
70
|
+
ContentfulMiddleman::ImportTask.new(space_name, content_type_names, content_type_mappers, instance)
|
71
|
+
end
|
72
|
+
|
73
|
+
def shared_instance
|
74
|
+
@shared_instance ||= ::Middleman::Application.server.inst
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module ContentfulMiddleman
|
2
|
+
class Context < BasicObject
|
3
|
+
def initialize
|
4
|
+
@variables = {}
|
5
|
+
@nexted_contexts = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def method_missing(symbol, *args, &block)
|
9
|
+
if symbol =~ /\A.+=\z/
|
10
|
+
variable_name = symbol.to_s.gsub('=','')
|
11
|
+
variable_value = args.first
|
12
|
+
|
13
|
+
set variable_name, variable_value
|
14
|
+
else
|
15
|
+
get symbol
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def nest(field_name)
|
20
|
+
@nexted_contexts << field_name
|
21
|
+
new_context = Context.new
|
22
|
+
yield new_context
|
23
|
+
|
24
|
+
set field_name, new_context
|
25
|
+
end
|
26
|
+
|
27
|
+
def map(field_name, elements)
|
28
|
+
@nexted_contexts << field_name
|
29
|
+
new_contexts = elements.map do |element|
|
30
|
+
new_context = Context.new
|
31
|
+
yield element, new_context
|
32
|
+
new_context
|
33
|
+
end
|
34
|
+
|
35
|
+
set field_name, new_contexts
|
36
|
+
end
|
37
|
+
|
38
|
+
def set(name, value)
|
39
|
+
@variables[name.to_sym] = value
|
40
|
+
end
|
41
|
+
|
42
|
+
def get(name)
|
43
|
+
@variables.fetch(name.to_sym)
|
44
|
+
end
|
45
|
+
|
46
|
+
def is_a?(klass)
|
47
|
+
Context == klass
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_hash
|
51
|
+
@variables
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_yaml
|
55
|
+
variables = @variables.dup
|
56
|
+
variables.update(variables) do |variable_name, variable_value|
|
57
|
+
if @nexted_contexts.include? variable_name
|
58
|
+
hashize_nested_context(variable_value)
|
59
|
+
else
|
60
|
+
variable_value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
variables.to_yaml
|
65
|
+
end
|
66
|
+
|
67
|
+
def hashize_nested_context(nested_context)
|
68
|
+
case nested_context
|
69
|
+
when ::Array
|
70
|
+
nested_context.map {|e| e.to_hash}
|
71
|
+
else
|
72
|
+
nested_context.to_hash
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -1,69 +1,71 @@
|
|
1
1
|
require 'middleman-core'
|
2
2
|
require 'contentful'
|
3
|
+
require_relative 'mappers/base'
|
4
|
+
require_relative 'helpers'
|
5
|
+
require_relative 'instance'
|
3
6
|
|
4
7
|
# The Contentful Middleman extensions allows to load managed content into Middleman projects through the Contentful Content Management Platform.
|
5
8
|
module ContentfulMiddleman
|
9
|
+
def self.instances
|
10
|
+
@contentful_middleman_instances ||= []
|
11
|
+
end
|
12
|
+
|
6
13
|
class Core < ::Middleman::Extension
|
7
|
-
|
8
|
-
slug: :id,
|
9
|
-
date: :created_at,
|
10
|
-
title: :id,
|
11
|
-
body: :id,
|
12
|
-
tags: :tags
|
13
|
-
}
|
14
|
+
self.supports_multiple_instances = true
|
14
15
|
|
15
|
-
|
16
|
+
option :space, nil,
|
17
|
+
'The Contentful Space ID and name'
|
16
18
|
|
17
|
-
option :
|
18
|
-
|
19
|
+
option :access_token, nil,
|
20
|
+
'The Contentful Content Delivery API access token'
|
19
21
|
|
20
|
-
option :
|
22
|
+
option :cda_query, {},
|
23
|
+
'The conditions that are used on the Content Delivery API to query for blog posts'
|
21
24
|
|
22
|
-
option :
|
23
|
-
|
25
|
+
option :content_types, {},
|
26
|
+
'The mapping of Content Types names to ids'
|
24
27
|
|
25
|
-
option :sync_blog_before_build, false, "Synchronize the blog from Contentful before the build phase"
|
26
28
|
|
27
|
-
|
28
|
-
super
|
29
|
+
helpers ContentfulMiddleman::Helpers
|
29
30
|
|
30
|
-
|
31
|
-
|
31
|
+
#
|
32
|
+
# Middleman hooks
|
33
|
+
#
|
34
|
+
def after_configuration
|
35
|
+
massage_options
|
32
36
|
|
33
|
-
|
34
|
-
contentful_middleman.sync_blog if contentful_middleman.middleman_blog_enabled? && contentful_middleman.options.sync_blog_before_build
|
35
|
-
end
|
37
|
+
ContentfulMiddleman.instances << (ContentfulMiddleman::Instance.new self)
|
36
38
|
end
|
37
39
|
|
38
|
-
|
39
|
-
def
|
40
|
-
|
40
|
+
private
|
41
|
+
def massage_options
|
42
|
+
massage_space_options
|
43
|
+
massage_content_types_options
|
41
44
|
end
|
42
45
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
46
|
+
def massage_space_options
|
47
|
+
space_option = options.space
|
48
|
+
space_name = space_option.keys.first
|
49
|
+
space_id = space_option.fetch(space_name)
|
48
50
|
|
49
|
-
|
50
|
-
@blog_post_mappings ||= ContentfulMiddleman::Core::DEFAULT_BLOG_MAPPINGS.merge(options.blog_post_mappings)
|
51
|
+
options.space = { name: space_name, id: space_id }
|
51
52
|
end
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
54
|
+
def massage_content_types_options
|
55
|
+
content_types_options = options.content_types
|
56
|
+
new_content_types_options = content_types_options.each_with_object({}) do |(content_type_name, value), options|
|
57
|
+
if value.is_a? Hash
|
58
|
+
mapper = value.fetch(:mapper)
|
59
|
+
id = value.fetch(:id)
|
60
|
+
else
|
61
|
+
mapper = Mapper::Base
|
62
|
+
id = value
|
63
|
+
end
|
61
64
|
|
62
|
-
|
63
|
-
# A helper method to access the Contentful Gem client
|
64
|
-
def contentful
|
65
|
-
contentful_middleman_client
|
65
|
+
options[id] = {name: content_type_name, mapper: mapper}
|
66
66
|
end
|
67
|
+
|
68
|
+
options.content_types = new_content_types_options
|
67
69
|
end
|
68
70
|
end
|
69
71
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ContentfulMiddleman
|
2
|
+
class ImportTask
|
3
|
+
def initialize(space_name, content_type_names, content_type_mappers, contentful)
|
4
|
+
@space_name = space_name
|
5
|
+
@content_type_names = content_type_names
|
6
|
+
@content_type_mappers = content_type_mappers
|
7
|
+
@changed_local_data = false
|
8
|
+
@contentful = contentful
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
old_version_hash = ContentfulMiddleman::VersionHash.read_for_space(@space_name)
|
13
|
+
|
14
|
+
LocalData::Store.new(local_data_files, @space_name).write
|
15
|
+
|
16
|
+
new_version_hash = ContentfulMiddleman::VersionHash.write_for_space_with_entries(@space_name, entries)
|
17
|
+
|
18
|
+
@changed_local_data = new_version_hash != old_version_hash
|
19
|
+
end
|
20
|
+
|
21
|
+
def changed_local_data?
|
22
|
+
@changed_local_data
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def local_data_files
|
27
|
+
entries.map do |entry|
|
28
|
+
content_type_mapper = @content_type_mappers.fetch(entry.content_type.id)
|
29
|
+
content_type_name = @content_type_names.fetch(entry.content_type.id).to_s
|
30
|
+
context = ContentfulMiddleman::Context.new
|
31
|
+
|
32
|
+
content_type_mapper.new.map(context, entry)
|
33
|
+
|
34
|
+
LocalData::File.new(context.to_yaml, File.join(@space_name, content_type_name, entry.id))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def entries
|
39
|
+
@entries ||= @contentful.entries
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module ContentfulMiddleman
|
2
|
+
class Instance
|
3
|
+
def initialize(extension)
|
4
|
+
@extension = extension
|
5
|
+
end
|
6
|
+
|
7
|
+
def entries
|
8
|
+
client.entries(options.cda_query)
|
9
|
+
end
|
10
|
+
|
11
|
+
def space_name
|
12
|
+
@space_name ||= options.space.fetch(:name)
|
13
|
+
end
|
14
|
+
|
15
|
+
def content_types_ids_to_mappers
|
16
|
+
@content_types_mappers ||= options.content_types.reduce({}) do |acc, (content_type_id, config)|
|
17
|
+
content_type_mapper = config.fetch(:mapper)
|
18
|
+
acc[content_type_id] = content_type_mapper
|
19
|
+
acc
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def content_types_ids_to_names
|
24
|
+
@content_types_names ||= options.content_types.reduce({}) do |acc, (content_type_id, config)|
|
25
|
+
content_type_name = config.fetch(:name)
|
26
|
+
acc[content_type_id] = content_type_name
|
27
|
+
acc
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def client
|
33
|
+
@client ||= Contentful::Client.new(
|
34
|
+
access_token: options.access_token,
|
35
|
+
space: options.space.fetch(:id),
|
36
|
+
dynamic_entries: :auto,
|
37
|
+
raise_errors: true
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def options
|
42
|
+
@extension.options
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ContentfulMiddleman
|
2
|
+
module LocalData
|
3
|
+
class File
|
4
|
+
class << self
|
5
|
+
def thor=(thor)
|
6
|
+
@thor = thor
|
7
|
+
end
|
8
|
+
|
9
|
+
def thor
|
10
|
+
@thor
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(data, path)
|
15
|
+
@data = data
|
16
|
+
@path = path
|
17
|
+
end
|
18
|
+
|
19
|
+
def write
|
20
|
+
self.class.thor.create_file(local_data_file_path, nil, {}) { @data }
|
21
|
+
end
|
22
|
+
|
23
|
+
def local_data_file_path
|
24
|
+
base_path = LocalData::Store.base_path
|
25
|
+
::File.join(base_path, @path)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ContentfulMiddleman
|
2
|
+
module LocalData
|
3
|
+
class Store
|
4
|
+
include ContentfulMiddleman::Tools::Backup::InstanceMethods
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def base_path=(path)
|
8
|
+
@base_path = path
|
9
|
+
end
|
10
|
+
|
11
|
+
def base_path
|
12
|
+
@base_path
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(files, folder)
|
17
|
+
@files = files
|
18
|
+
@folder = folder
|
19
|
+
end
|
20
|
+
|
21
|
+
def write
|
22
|
+
do_with_backup backup_name, path_to_backup do
|
23
|
+
@files.each(&:write)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def backup_name
|
29
|
+
"#{@folder}-data-backup"
|
30
|
+
end
|
31
|
+
|
32
|
+
def path_to_backup
|
33
|
+
::File.join(self.class.base_path, @folder)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ContentfulMiddleman
|
2
|
+
module Mapper
|
3
|
+
class Base
|
4
|
+
def map(context, entry)
|
5
|
+
map_entry(context, entry)
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
def map_entry(context, entry)
|
10
|
+
context.id = entry.id
|
11
|
+
entry.fields.each {|k, v| map_field context, k, v}
|
12
|
+
end
|
13
|
+
|
14
|
+
def map_field(context, field_name, field_value)
|
15
|
+
case field_value
|
16
|
+
when Contentful::Asset
|
17
|
+
map_asset(context, field_name, field_value)
|
18
|
+
when Array
|
19
|
+
map_array(context, field_name, field_value)
|
20
|
+
else
|
21
|
+
context.set(field_name, field_value)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def map_asset(context, field_name, field_value)
|
26
|
+
context.nest(field_name) do |nested_context|
|
27
|
+
nested_context.title = field_value.title
|
28
|
+
nested_context.url = field_value.file.url
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def map_array(context, field_name, field_value)
|
33
|
+
context.map(field_name, field_value) do |element, new_context|
|
34
|
+
map_entry(new_context, element)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module ContentfulMiddleman
|
4
|
+
module Tools
|
5
|
+
class NullBackup
|
6
|
+
def restore; end
|
7
|
+
def destroy; end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Backup
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def basepath
|
14
|
+
::File.join ENV["MM_ROOT"], ".tmp", "backups"
|
15
|
+
end
|
16
|
+
|
17
|
+
def ensure_backup_path!
|
18
|
+
return if ::File.exists? basepath
|
19
|
+
|
20
|
+
FileUtils.mkdir_p basepath
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def initialize(name, source)
|
26
|
+
@name = name
|
27
|
+
@source = source
|
28
|
+
|
29
|
+
self.class.ensure_backup_path!
|
30
|
+
|
31
|
+
FileUtils.mkdir(path)
|
32
|
+
FileUtils.mv(source, path)
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def restore
|
37
|
+
FileUtils.rm_rf(@source)
|
38
|
+
FileUtils.mv(path, @source)
|
39
|
+
end
|
40
|
+
|
41
|
+
def destroy
|
42
|
+
FileUtils.rm_rf(path)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def path
|
47
|
+
::File.join self.class.basepath, name_and_date
|
48
|
+
end
|
49
|
+
|
50
|
+
def all_files_in_path(path)
|
51
|
+
Dir.glob(::File.join(path, "*"))
|
52
|
+
end
|
53
|
+
|
54
|
+
def name_and_date
|
55
|
+
@name_and_date ||= "#{@name}-#{Time.now.strftime("%Y%m%d%H%M%S")}"
|
56
|
+
end
|
57
|
+
|
58
|
+
module InstanceMethods
|
59
|
+
def do_with_backup(backup_name, path_to_backup)
|
60
|
+
backup = create_backup backup_name, path_to_backup
|
61
|
+
remove_backup = false
|
62
|
+
|
63
|
+
begin
|
64
|
+
yield
|
65
|
+
remove_backup = true
|
66
|
+
rescue StandardError => e
|
67
|
+
backup.restore
|
68
|
+
remove_backup = true
|
69
|
+
raise e
|
70
|
+
ensure
|
71
|
+
backup.destroy if remove_backup
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
def create_backup(backup_name, path_to_backup)
|
77
|
+
if ::File.exist? path_to_backup
|
78
|
+
Backup.new(backup_name, path_to_backup)
|
79
|
+
else
|
80
|
+
NullBackup.new
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ContentfulMiddleman
|
2
|
+
class VersionHash
|
3
|
+
class << self
|
4
|
+
def source_root=(source_root)
|
5
|
+
@source_root = source_root
|
6
|
+
end
|
7
|
+
|
8
|
+
def read_for_space(space_name)
|
9
|
+
hashfilename_for_space = hashfilename(space_name)
|
10
|
+
::File.read(hashfilename_for_space) if File.exist? hashfilename_for_space
|
11
|
+
end
|
12
|
+
|
13
|
+
def write_for_space_with_entries(space_name, entries)
|
14
|
+
sorted_entries = entries.sort {|a, b| a.id <=> b.id}
|
15
|
+
ids_and_revisions_string = sorted_entries.map {|e| "#{e.id}#{e.revision}"}.join
|
16
|
+
entries_hash = Digest::SHA1.hexdigest( ids_and_revisions_string )
|
17
|
+
|
18
|
+
File.open(hashfilename(space_name), 'w') { |file| file.write(entries_hash) }
|
19
|
+
|
20
|
+
entries_hash
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def hashfilename(space_name)
|
25
|
+
::File.join(@source_root, ".#{space_name}-space-hash")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
metadata
CHANGED
@@ -1,67 +1,69 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: contentful_middleman
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sascha Konietzke
|
8
|
+
- Farruco Sanjurjoj
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
+
date: 2015-01-26 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: middleman-core
|
15
16
|
requirement: !ruby/object:Gem::Requirement
|
16
17
|
requirements:
|
17
|
-
- - ~>
|
18
|
+
- - "~>"
|
18
19
|
- !ruby/object:Gem::Version
|
19
20
|
version: '3.3'
|
20
21
|
type: :runtime
|
21
22
|
prerelease: false
|
22
23
|
version_requirements: !ruby/object:Gem::Requirement
|
23
24
|
requirements:
|
24
|
-
- - ~>
|
25
|
+
- - "~>"
|
25
26
|
- !ruby/object:Gem::Version
|
26
27
|
version: '3.3'
|
27
28
|
- !ruby/object:Gem::Dependency
|
28
29
|
name: contentful
|
29
30
|
requirement: !ruby/object:Gem::Requirement
|
30
31
|
requirements:
|
31
|
-
- -
|
32
|
+
- - ">="
|
32
33
|
- !ruby/object:Gem::Version
|
33
34
|
version: '0'
|
34
35
|
type: :runtime
|
35
36
|
prerelease: false
|
36
37
|
version_requirements: !ruby/object:Gem::Requirement
|
37
38
|
requirements:
|
38
|
-
- -
|
39
|
+
- - ">="
|
39
40
|
- !ruby/object:Gem::Version
|
40
41
|
version: '0'
|
41
42
|
- !ruby/object:Gem::Dependency
|
42
43
|
name: rubygems-tasks
|
43
44
|
requirement: !ruby/object:Gem::Requirement
|
44
45
|
requirements:
|
45
|
-
- - ~>
|
46
|
+
- - "~>"
|
46
47
|
- !ruby/object:Gem::Version
|
47
48
|
version: '0.2'
|
48
49
|
type: :development
|
49
50
|
prerelease: false
|
50
51
|
version_requirements: !ruby/object:Gem::Requirement
|
51
52
|
requirements:
|
52
|
-
- - ~>
|
53
|
+
- - "~>"
|
53
54
|
- !ruby/object:Gem::Version
|
54
55
|
version: '0.2'
|
55
56
|
description: Load blog posts and other managed content into Middleman
|
56
57
|
email:
|
57
58
|
- sascha@contentful.com
|
59
|
+
- madtrick@gmail.com
|
58
60
|
executables: []
|
59
61
|
extensions: []
|
60
62
|
extra_rdoc_files: []
|
61
63
|
files:
|
62
|
-
- .gitignore
|
63
|
-
- .travis.yml
|
64
|
-
- .yardopts
|
64
|
+
- ".gitignore"
|
65
|
+
- ".travis.yml"
|
66
|
+
- ".yardopts"
|
65
67
|
- CHANGELOG.md
|
66
68
|
- CONTRIBUTING.md
|
67
69
|
- Gemfile
|
@@ -71,10 +73,18 @@ files:
|
|
71
73
|
- TODO
|
72
74
|
- contentful_middleman.gemspec
|
73
75
|
- lib/contentful_middleman.rb
|
74
|
-
- lib/contentful_middleman/commands/
|
75
|
-
- lib/contentful_middleman/commands/
|
76
|
+
- lib/contentful_middleman/commands/contentful.rb
|
77
|
+
- lib/contentful_middleman/commands/context.rb
|
76
78
|
- lib/contentful_middleman/core.rb
|
79
|
+
- lib/contentful_middleman/helpers.rb
|
80
|
+
- lib/contentful_middleman/import_task.rb
|
81
|
+
- lib/contentful_middleman/instance.rb
|
82
|
+
- lib/contentful_middleman/local_data/file.rb
|
83
|
+
- lib/contentful_middleman/local_data/store.rb
|
84
|
+
- lib/contentful_middleman/mappers/base.rb
|
85
|
+
- lib/contentful_middleman/tools/backup.rb
|
77
86
|
- lib/contentful_middleman/version.rb
|
87
|
+
- lib/contentful_middleman/version_hash.rb
|
78
88
|
- lib/middleman_extension.rb
|
79
89
|
- spec/contentful_middleman_spec.rb
|
80
90
|
- spec/spec_helper.rb
|
@@ -88,17 +98,17 @@ require_paths:
|
|
88
98
|
- lib
|
89
99
|
required_ruby_version: !ruby/object:Gem::Requirement
|
90
100
|
requirements:
|
91
|
-
- -
|
101
|
+
- - ">="
|
92
102
|
- !ruby/object:Gem::Version
|
93
103
|
version: '0'
|
94
104
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
105
|
requirements:
|
96
|
-
- -
|
106
|
+
- - ">="
|
97
107
|
- !ruby/object:Gem::Version
|
98
108
|
version: '0'
|
99
109
|
requirements: []
|
100
110
|
rubyforge_project:
|
101
|
-
rubygems_version: 2.
|
111
|
+
rubygems_version: 2.2.2
|
102
112
|
signing_key:
|
103
113
|
specification_version: 4
|
104
114
|
summary: Include mangablable content from the Contentful CMS and API into your Middleman
|
@@ -1,88 +0,0 @@
|
|
1
|
-
require 'middleman-core/cli'
|
2
|
-
require 'date'
|
3
|
-
require 'middleman-blog/uri_templates'
|
4
|
-
|
5
|
-
module Middleman
|
6
|
-
module Cli
|
7
|
-
# This class provides an "contentful" command for the middleman CLI.
|
8
|
-
class SyncBlog < Thor
|
9
|
-
include Thor::Actions
|
10
|
-
include ::Middleman::Blog::UriTemplates
|
11
|
-
|
12
|
-
check_unknown_options!
|
13
|
-
|
14
|
-
namespace :contentful
|
15
|
-
|
16
|
-
def self.source_root
|
17
|
-
ENV['MM_ROOT']
|
18
|
-
end
|
19
|
-
|
20
|
-
# Tell Thor to exit with a nonzero exit code on failure
|
21
|
-
def self.exit_on_failure?
|
22
|
-
true
|
23
|
-
end
|
24
|
-
|
25
|
-
desc "contentful", "Synchronize Contentful blog posts"
|
26
|
-
method_option "lang",
|
27
|
-
aliases: "-l",
|
28
|
-
desc: "The language to create the post with (defaults to I18n.default_locale if avaliable)"
|
29
|
-
method_option "blog",
|
30
|
-
aliases: "-b",
|
31
|
-
desc: "The name of the blog to create the post inside (for multi-blog apps, defaults to the only blog in single-blog apps)"
|
32
|
-
def contentful
|
33
|
-
contentful_middleman = shared_instance.contentful_middleman
|
34
|
-
client = shared_instance.contentful_middleman_client
|
35
|
-
contentful_middleman_options = contentful_middleman.options
|
36
|
-
blog_post_mappings = contentful_middleman.blog_post_mappings
|
37
|
-
|
38
|
-
if shared_instance.respond_to? :blog
|
39
|
-
shared_instance.logger.info " Contentful Sync: Start..."
|
40
|
-
|
41
|
-
client.entries(contentful_middleman_options.blog_posts_query).each do |entry|
|
42
|
-
slug = value_from_object(entry, blog_post_mappings[:slug])
|
43
|
-
title = value_from_object(entry, blog_post_mappings[:title])
|
44
|
-
date = value_from_object(entry, blog_post_mappings[:date]).strftime("%Y-%m-%d")
|
45
|
-
tags = value_from_object(entry, blog_post_mappings[:tags]) || []
|
46
|
-
body = value_from_object(entry, blog_post_mappings[:body])
|
47
|
-
|
48
|
-
@title = title
|
49
|
-
@slug = slug || safe_parameterize(title)
|
50
|
-
@date = date ? Time.zone.parse(date) : Time.zone.now
|
51
|
-
@tags = tags
|
52
|
-
@lang = options[:lang] || ( I18n.default_locale if defined? I18n )
|
53
|
-
@body = body
|
54
|
-
|
55
|
-
blog_inst = shared_instance.blog(options[:blog])
|
56
|
-
|
57
|
-
path_template = blog_inst.source_template
|
58
|
-
params = date_to_params(@date).merge(lang: @lang.to_s, title: @slug)
|
59
|
-
article_path = apply_uri_template path_template, params
|
60
|
-
|
61
|
-
template contentful_middleman.options.new_article_template, File.join(shared_instance.source_dir, article_path + blog_inst.options.default_extension)
|
62
|
-
end
|
63
|
-
|
64
|
-
shared_instance.logger.info " Contentful Sync: Done!"
|
65
|
-
else
|
66
|
-
raise Thor::Error.new "You need to activate the blog extension in config.rb before you can create an article"
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
private
|
71
|
-
def shared_instance
|
72
|
-
@shared_instance ||= ::Middleman::Application.server.inst
|
73
|
-
end
|
74
|
-
|
75
|
-
def value_from_object(object, mapping)
|
76
|
-
if ( mapping.is_a?(Symbol) || mapping.is_a?(String) ) && object.respond_to?(mapping)
|
77
|
-
object.send(mapping)
|
78
|
-
elsif mapping.is_a?(Proc)
|
79
|
-
object.instance_exec(object, &mapping)
|
80
|
-
else
|
81
|
-
shared_instance.logger.warn "Warning - Unknown mapping (#{mapping}) for object (#{object.class}) with ID (#{object.id})"
|
82
|
-
nil
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
end
|
88
|
-
end
|