contentful_middleman 0.0.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/contentful/contentful_middleman.png)](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
|