├── .gitignore
├── LICENSE
├── README.md
├── app
├── .htaccess
├── AppCache.php
├── AppKernel.php
├── Resources
│ └── views
│ │ ├── base.html.twig
│ │ ├── default
│ │ ├── contact-success.html.twig
│ │ ├── index.html.twig
│ │ └── report.html.twig
│ │ ├── emails
│ │ ├── contact.html.twig
│ │ └── report.html.twig
│ │ ├── forms
│ │ ├── article.html.twig
│ │ ├── contact.html.twig
│ │ ├── email-report-success.html.twig
│ │ ├── email-report.html.twig
│ │ └── subscriber.html.twig
│ │ └── snippets
│ │ ├── flickr-ajax-snippet.html.twig
│ │ ├── flickr-images.html.twig
│ │ ├── loading-animation.html.twig
│ │ └── pinterest.html.twig
├── autoload.php
└── config
│ ├── config.yml
│ ├── config_dev.yml
│ ├── config_prod.yml
│ ├── config_test.yml
│ ├── example-parameters.yml
│ ├── routing.yml
│ ├── routing_dev.yml
│ ├── security.yml
│ └── services.yml
├── bin
├── console
└── symfony_requirements
├── composer.json
├── composer.lock
├── doc
└── img
│ └── symfony-optimizer-splash.png
├── phpunit.xml.dist
├── runTests.sh
├── src
├── .htaccess
└── AppBundle
│ ├── AppBundle.php
│ ├── Classes
│ ├── AnalysisHelper.php
│ ├── Analyzer.php
│ ├── Bitly.php
│ ├── CurlHelper.php
│ └── Flickr.php
│ ├── Controller
│ └── DefaultController.php
│ ├── DependencyInjection
│ └── ConfigValidator.php
│ ├── Entity
│ ├── Article.php
│ ├── ContactRequest.php
│ ├── ReportEmailRecipient.php
│ └── Subscriber.php
│ └── Twig
│ └── AppExtension.php
├── tests
└── AppBundle
│ └── Controller
│ └── DefaultControllerTest.php
├── var
├── SymfonyRequirements.php
├── cache
│ └── .gitkeep
├── logs
│ └── .gitkeep
└── sessions
│ └── .gitkeep
└── web
├── .htaccess
├── app.php
├── app_dev.php
├── apple-touch-icon.png
├── config.php
├── css
├── base
│ ├── bootstrap.min.css
│ ├── bootstrap.min.css.map
│ ├── font-awesome.min.css
│ └── reset.css
├── index
│ ├── bubbles.css
│ ├── how-it-works.css
│ ├── index.css
│ └── loading.css
├── main.css
└── report
│ └── report.css
├── favicon.ico
├── fonts
├── FontAwesome.otf
├── fontawesome-webfont.eot
├── fontawesome-webfont.svg
├── fontawesome-webfont.ttf
├── fontawesome-webfont.woff
├── fontawesome-webfont.woff2
├── glyphicons-halflings-regular.eot
├── glyphicons-halflings-regular.svg
├── glyphicons-halflings-regular.ttf
├── glyphicons-halflings-regular.woff
└── glyphicons-halflings-regular.woff2
├── img
├── ads
│ ├── MarketSamuraiBanner.jpg
│ ├── bluehost
│ │ └── 300x250
│ │ │ ├── bh_300x250_01.gif
│ │ │ ├── bh_300x250_02.gif
│ │ │ ├── bh_300x250_03.gif
│ │ │ ├── bh_300x250_04.jpg
│ │ │ └── bh_300x250_05.gif
│ ├── elegantthemes
│ │ └── 300x250.gif
│ └── semrush_banner_300_250.png
├── article-optimizer-example-report.png
├── article-optimizer-logo.png
├── backgrounds
│ ├── groovepaper.png
│ └── navy_blue.png
├── badges
│ ├── 100-percent-optimized
│ │ ├── blue-oval-100-percent-optimized.png
│ │ ├── brown-100-percent-optimized.png
│ │ ├── brown-leather-100-percent-optimized.png
│ │ ├── excellent-article-award.png
│ │ ├── excellent-content-award.png
│ │ ├── excellent-content.png
│ │ ├── fantastic-article-award.png
│ │ ├── fantastic-content-badge.png
│ │ ├── green-100-percent-optimized.png
│ │ ├── optimal-article.png
│ │ ├── optimized-content-badge.png
│ │ ├── optimized.png
│ │ ├── perfect-article-award.png
│ │ ├── perfect-article.png
│ │ ├── seo-optimized-article.png
│ │ ├── silver-circle-100-percent-optimized.png
│ │ ├── super-article-award.png
│ │ ├── super-content.png
│ │ ├── superb-content-award.png
│ │ ├── well-done-article-award.png
│ │ ├── winning-article-badge.png
│ │ ├── winning-content-badge.png
│ │ └── winning-writing.png
│ └── categories
│ │ ├── arts-entertainment
│ │ └── art-entertainment-award.png
│ │ ├── business
│ │ └── business-article-award.png
│ │ ├── computer-internet
│ │ └── computers-internet-award.png
│ │ ├── culture-politics
│ │ └── culture-politics-badge.png
│ │ ├── gaming
│ │ └── gaming-award.png
│ │ ├── health
│ │ └── health-content-award.png
│ │ ├── law-crime
│ │ └── law-and-crime-badge.png
│ │ ├── recreation
│ │ └── recreation-badge.png
│ │ ├── religion
│ │ └── religion-badge.png
│ │ ├── science-technology
│ │ └── science.png
│ │ ├── sports
│ │ └── sports-award.png
│ │ ├── unknown
│ │ └── default-article-badge.png
│ │ └── weather
│ │ └── weather-badge.png
├── favicon.png
└── loading.gif
├── js
├── bootstrap.min.js
├── clipboard.min.js
├── index.js
├── jquery-2.2.4.min.js
├── optimizer-client.js
├── report-client.js
└── report.js
└── robots.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | .phpintel/
2 | /app/config/parameters.yml
3 | /build/
4 | /phpunit.xml
5 | /var/*
6 | !/var/cache
7 | /var/cache/*
8 | !var/cache/.gitkeep
9 | !/var/logs
10 | /var/logs/*
11 | !var/logs/.gitkeep
12 | !/var/sessions
13 | /var/sessions/*
14 | !var/sessions/.gitkeep
15 | !var/SymfonyRequirements.php
16 | /vendor/
17 | /web/bundles/
18 | /web/saved-reports/*
19 | /web/.sass-cache
20 | *.sublime-project
21 | *.sublime-workspace
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Zack Proser
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [Article Optimizer](https://www.article-optimize.com)
2 |
3 | 
4 |
5 | ## Overview
6 |
7 | This application allows users to submit text articles on any subject and have their content analyzed. This application is live and free to use at [www.article-optimize.com](https://www.article-optimize.com). For a more in-depth write-up of this application, read [my technical blog post about it here](https://www.zackproser.com/blog/article/I-Open-Sourced-My-Content-Analysis-Tool).
8 |
9 | The resulting report will make suggestions on keyword, phrase and sentiment optimizations that the author can make to improve the legibility and hopefully the overall rankings of their work. The report will also search for high quality on-topic images that are
10 | copyright-free, allowing the author to quickly drop them into their content to improve its media richness.
11 |
12 | This project was open-sourced in order to:
13 | * give back to the open-source community
14 | * provide a demonstration of a non-trivial Symfony web app
15 | * demonstrate a use case for IBM's Watson linguistic processing API
16 |
17 | This application, its assets and all associated documentation was created by Zack Proser:
18 |
19 | * [Portfolio site](https://www.zackproser.com)
20 | * [Email](mailto:zackproser@gmail.com)
21 | * [Github](https://www.github.com/zackproser)
22 |
23 | ## Application at a Glance
24 |
25 | ### Content Analysis
26 | The Article Optimizer is a search engine optimization tool for content authors. It provides a useful free service in providing authors with detailed analysis of their unique content, leveraging IBM's Watson linguistic analysis functionality, along with suggestions for improving long term search rankings.
27 |
28 | ### Persistent Reports
29 | Article reports are written to the server as soon as they are complete, so that they can be looked up in perpetuity and served quickly, since they are static HTML pages.
30 |
31 | ### Targeted Content Marketing
32 | In exchange, the application can gather user email addresses and subscribe them to a mailchimp list for longterm engagement and marketing of related products, if the user provides their consent to receive such communications.
33 |
34 | ### Display Advertising
35 | In addition, the application can be configured to display two separate advertising modules, each featuring 2 separate advertisements, integrated into the user's content report for the purposes of affiliate marketing or direct marketing of the application's owners' products or services.
36 |
37 | ### Social Media Sharing
38 | To maximize social sharing of reports, each report gets its own programmatically generated Bitly link so that sharing and traffic can be monitored at a granular report level. Users can also share their report (and thereby the application itself) via Twitter, Pinterest, Facebook or by emailing the report directly to a friend or colleague, all with one click.
39 |
40 | ### Analytics
41 | The application allows you to drop-in the Google Analytics property id you would like associated, allowing you to track usage patterns and traffic across all pages and reports with no additional effort or custom development.
42 |
43 | ### Services Integrated
44 |
45 | The Article Optimizer leverages
46 |
47 | * [Alchemy API - now part of IBM's Watson cloud](https://www.ibm.com/watson/developercloud/alchemy-language.html)
48 | * [Mailchimp](https://mailchimp.com/)
49 | * [Flickr](https://www.flickr.com/)
50 | * [Google Analytics](https://google.com/analytics)
51 | * [Bitly](https://bitly.com)
52 | * [Pinterest](https://pinterest.com)
53 | * [Facebook](https://www.facebook.com)
54 | * [Twitter](https://www.twitter.com)
55 |
56 | ### Tech Stack
57 | This application was built using:
58 |
59 | * [Symfony 3.1](https://symfony.com)
60 | * [PHP 7](https://php.net)
61 | * [Bootstrap](https://getbootstrap.com)
62 | * [jQuery](https://jquery.com)
63 | * [nginx](https://nginx.com)
64 | * [Linode VPS](https://linode.com)
65 |
--------------------------------------------------------------------------------
/app/.htaccess:
--------------------------------------------------------------------------------
1 |
2 | Require all denied
3 |
4 |
5 | Order deny,allow
6 | Deny from all
7 |
8 |
--------------------------------------------------------------------------------
/app/AppCache.php:
--------------------------------------------------------------------------------
1 | getEnvironment(), ['dev', 'test'], true)) {
23 | $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
24 | $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
25 | $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
26 | $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
27 | }
28 |
29 | return $bundles;
30 | }
31 |
32 | public function getRootDir()
33 | {
34 | return __DIR__;
35 | }
36 |
37 | public function getCacheDir()
38 | {
39 | return dirname(__DIR__).'/var/cache/'.$this->getEnvironment();
40 | }
41 |
42 | public function getLogDir()
43 | {
44 | return dirname(__DIR__).'/var/logs';
45 | }
46 |
47 | public function registerContainerConfiguration(LoaderInterface $loader)
48 | {
49 | $loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/Resources/views/base.html.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
30 |
31 | {% if analysis.success is defined and analysis.success %}
32 |
33 | {# Render the first ribbon, passing the second paramter 'first' #}
34 | {# which will result in the 'body' padding class most ribbons use being omitted #}
35 | {{ ribbon('Your ' ~ analysis.category ~ ' Article Analysis', 'first') }}
36 |
37 | {# Begin initial presentation and sharing section #}
38 |
39 |
40 | {# Begin Badge & Text Pedestal Row #}
41 |
42 |
43 |
44 | {# If the analysis was successful, render the report data #}
45 | {% if analysis.badge is defined and analysis.badge %}
46 |
47 | {% endif %}
48 |
49 |
50 | {% if analysis.articleBody is defined %}
51 |
Your Article:
52 |
53 |
54 | {{ analysis.articleBody }}
55 |
56 |
57 | {% endif %}
58 |
59 |
60 |
61 |
62 | {# We have a successful analysis - render sharing info #}
63 |
64 |
65 | This is your permanent article report. You can share the link with friends or e-mail it to clients to show them the quality of your work.
66 |
67 |
68 |
69 |
70 |
71 | {# End Badge & Text Pedestal Row #}
72 |
73 | {# Begin Sharing Row #}
74 |
75 |
76 |
77 |
78 |
Either copy the direct link...
79 |
80 | {# Begin Shortlink Section #}
81 |
82 |
88 | {# End Shortlink Section #}
89 |
90 |
91 |
92 |
93 |
Or click one of these sharing buttons
94 |
101 |
102 |
103 |
104 |
{# End Sharing Row #}
105 |
106 |
{# End container-fluid #}
107 |
108 |
{# End Jumbotron First Report Backing #}
109 |
110 | {{ ribbon('Feature Analysis') }}
111 |
112 | {# Begin Article Stats Section #}
113 |
114 |
115 |
116 |
117 |
118 | Feature
119 | Search Engines See As
120 | Analysis
121 |
122 |
123 |
134 | Total Word Count
135 |
136 | {% if analysis.wordCount is defined %}
137 | {{ analysis.wordCount }}
138 | {% endif %}
139 |
140 |
141 | {% if analysis.wordCount is defined %}
142 | {% if analysis.wordCount <= 250 %}
143 | Poor
144 | {% elseif analysis.wordCount <= 500 %}
145 | Okay
146 | {% elseif analysis.wordCount > 500 %}
147 | Great
148 | {% endif %}
149 | {% endif %}
150 |
151 |
152 | {% if analysis.categorySucceeded is defined and analysis.categorySucceeded %}
153 |
154 | Content Category
155 | {{ analysis.category }}
156 |
157 |
158 | {% endif %}
159 |
160 |
161 |
162 |
163 | {# If concepts analysis succeeded, render concept table #}
164 | {% if analysis.conceptsSucceeded is defined and analysis.conceptsSucceeded %}
165 |
166 | Roughly speaking, these are the concepts that search engines will consider most important in your article.
167 |
168 |
169 |
170 | It can be useful to scope out content that is already published and competing for the same keywords. Click the "View Competing URLs" link to see the top-performing content for each concept.
171 |
172 |
173 |
174 |
175 | Concept
176 | Relevance
177 | Competing URLs
178 |
179 |
180 | {% for concept in analysis.concepts %}
181 |
182 |
183 | {% if concept.text is defined %}
184 | {{ concept.text }}
185 | {% endif %}
186 |
187 |
188 | {% if concept.relevance is defined %}
189 | {{ concept.relevance }}
190 | {% endif %}
191 |
192 |
193 | {% if concept.text is defined %}
194 | View Competing URLs
195 | {% endif %}
196 |
197 |
198 | {% endfor %}
199 |
200 |
201 |
202 | {% endif %}
203 | {# End concepts table #}
204 |
205 |
{# End Jumbotron Report Backing #}
206 |
207 | {# Begin Keywords Section #}
208 | {% if analysis.keywordsSucceeded is defined and analysis.keywordsSucceeded %}
209 | {{ ribbon('Keyword Sentiment Analysis') }}
210 |
211 |
212 |
213 | This section breaks down whether the top keywords in your article have positive, negative or neutral sentiment or attitude attached to them.
214 |
215 |
216 |
217 | It can be useful to spot-check this section to ensure that your descriptions of your concepts are reflective of your intention.
218 |
219 |
220 | {{ legend('Keyword Sentiment Legend', ['positive', 'neutral', 'negative']) }}
221 |
222 |
223 |
224 |
225 | Keyword
226 | Relevance
227 | Sentiment
228 |
229 |
230 | {% for keyword in analysis.keywords %}
231 |
242 | {% if keyword.text is defined %}{{ keyword.text }}{% endif %}
243 | {% if keyword.relevance is defined %}{{ keyword.relevance }}{% endif %}
244 |
245 | {% for type, score in keyword.sentiment %}
246 | {% if type is defined %}
247 | {{ type|replace({ ('type'): '' }) }}
248 | {% endif %}
249 | {% if score is defined %}
250 | {{ score|replace({ ('score'): '' }) }}
251 | {% endif %}
252 | {% endfor %}
253 |
254 |
255 | {% endfor %}
256 |
257 |
258 |
259 | {# Begin Suggested Keywords Section #}
260 | {% if analysis.suggestedKeywordsSucceeded is defined and analysis.suggestedKeywordsSucceeded %}
261 |
You can increase traffic to your article by working in these high-traffic keywords in a natural and non-spammy way.
262 |
263 | {% for suggestion in analysis.suggestedKeywords %}
264 | {{ suggestion }}
265 | {% endfor %}
266 |
267 | {% endif %}
268 | {# End Suggested Keywords Section #}
269 |
270 | {% endif %}
271 | {# End Keywords Section #}
272 |
273 |
274 |
275 | {# Begin Advertisement Block #1 #}
276 | {% if analysis.adsEnabled is defined and analysis.adsEnabled %}
277 |
278 | {{ ribbon('Recommended Tools') }}
279 |
280 | {{ advertisement(analysis.ads[0], analysis.ads[1]) }}
281 |
282 | {% endif %}
283 | {# End Advertisement Block #1 #}
284 |
285 | {# Begin Entities Analysis #}
286 | {% if analysis.entitiesSucceeded is defined and analysis.entitiesSucceeded %}
287 | {{ ribbon('Entities Sentiment Analysis') }}
288 |
289 | {# Begin Entities Section #}
290 |
291 |
292 | This section breaks down the positive, negative, and neutral associations your article attaches to its top entities.
293 |
294 |
295 | Make sure these square with your intended message.
296 |
297 |
298 | {{ legend('Entities Sentiment Legend', ['positive', 'neutral', 'negative']) }}
299 |
300 |
301 |
302 | Entity
303 | Type
304 | Relevance
305 | Sentiment
306 | Count
307 |
308 |
309 | {% for entity in analysis.entities %}
310 |
321 |
322 | {% if entity.text is defined %}
323 | {{ entity.text }}
324 | {% endif %}
325 |
326 |
327 | {% if entity.type is defined %}
328 | {{ entity.type }}
329 | {% endif %}
330 |
331 |
332 | {% if entity.relevance is defined %}
333 | {{ entity.relevance }}
334 | {% endif %}
335 |
336 |
337 | {% if entity.sentiment is defined %}
338 | {% for type, score in entity.sentiment %}
339 | {% if type is defined %}
340 | {{ type|replace({ ('type'): '' }) }}
341 | {% endif %}
342 | {% if score is defined %}
343 | {{ score|replace({ ('score'): '' }) }}
344 | {% endif %}
345 | {% endfor %}
346 | {% endif %}
347 |
348 |
349 | {% if entity.count is defined %}
350 | {{ entity.count }}
351 | {% endif %}
352 |
353 |
354 | {% endfor %}
355 |
356 |
357 |
358 |
359 | {% endif %}
360 | {# End Entities Analysis Section #}
361 |
362 | {# Begin Phrase Density Section #}
363 | {% if analysis.phraseDensitySucceeded is defined and analysis.phraseDensitySucceeded %}
364 | {{ ribbon('Phrase Density') }}
365 |
366 |
367 | Phrase density describes how often the key phrases in your article appear. Your writing should be natural: search engines can detect "keyword stuffing", or artificially including a keyword or phrase too frequently.
368 |
369 |
370 |
371 | You should strongly consider using synonyms for any phrase highlighted in red here.
372 |
373 |
374 | {{ legend('Density Legend', ['Good', 'Low', 'Stuffing']) }}
375 |
376 |
377 |
378 | Phrase
379 | Frequency
380 |
381 |
382 | {% for level, array in analysis.phraseDensity %}
383 | {% for phrase, percentage in array %}
384 |
385 | {{ phrase }}
386 | {{ percentage }}
387 |
388 | {% endfor %}
389 | {% endfor %}
390 |
391 |
392 |
393 |
394 | {% endif %}
395 | {# End Phrase Density Section #}
396 |
397 | {# Begin Advertisement Block #2 #}
398 | {% if analysis.adsEnabled is defined and analysis.adsEnabled %}
399 |
400 | {{ ribbon('Content Boosting Services') }}
401 |
402 | {{ advertisement(analysis.ads[2], analysis.ads[3]) }}
403 |
404 | {% endif %}
405 | {# End Advertisement Block #2 #}
406 |
407 | {# Begin Images Section #}
408 | {{ ribbon('Copyright-Free Images') }}
409 |
410 |
411 | {# Render the images description loading panel if we have searchable keywords #}
412 | {% if analysis.flickrKeywords is defined and analysis.flickrKeywords|length > 0 %}
413 |
414 |
415 | These images have been selected because they were published with creative usage licenses that allow you to include them in your article.
416 |
417 |
418 | Including high-quality on-topic images in your content is a great way to stand out, increase inbound links to your content, and improve your article's search engine ranking.
419 |
420 |
421 |
422 |
423 |
424 |
Searching for Copyright Free Images...
425 |
426 |
427 | {% else %}
428 | {# Otherwise, render an error #}
429 |
Sorry! We weren't able to find any images for this topic!
430 | {% endif %}
431 |
432 | {# End Images Section #}
433 |
434 | {# Send Report by Email Modal #}
435 |
436 |
437 |
438 |
441 |
442 |
443 |
444 | {{ render(controller('AppBundle:Default:emailReport')) }}
445 |
Cancel
446 |
447 |
448 |
449 |
450 | {# End Repot by Email Modal #}
451 |
452 | {# Load Pinit.js for creating a Pinterest Pin It button #}
453 | {{ include('snippets/pinterest.html.twig') }}
454 |
455 | {# Load Flickr Ajax Snippet to fetch images based on concepts #}
456 | {{ include('snippets/flickr-ajax-snippet.html.twig', {keywords: analysis.flickrKeywords }) }}
457 | {% else %}
458 | {# Otherwise, analysis failef for some reason - render an error state #}
459 |
460 |
461 |
Oops! Something went horribly wrong!
462 |
We apologize, but we are unable to process your report right now.
463 |
Please ensure you have submitted the entire body text of your article, or please try again later.
464 |
Go Back and Try Again
465 |
466 | {% endif %}
467 |
468 | {% endblock %}
--------------------------------------------------------------------------------
/app/Resources/views/emails/contact.html.twig:
--------------------------------------------------------------------------------
1 | {{ message }}
2 |
3 | {{requestor}}
4 |
--------------------------------------------------------------------------------
/app/Resources/views/emails/report.html.twig:
--------------------------------------------------------------------------------
1 |
Someone sent you an article report!
2 |
3 |
Article Optimizer is a free tool for:
4 |
5 |
6 | Improving content quality
7 | Identifying issues prior to publishing
8 | Finding copyright-free images on-topic
9 |
--------------------------------------------------------------------------------
/app/Resources/views/forms/article.html.twig:
--------------------------------------------------------------------------------
1 | {# Begin Main Article Input Form #}
2 | {{ form_start(articleForm, {'name': 'articleForm'}) }}
3 |
4 | {{ form_row(articleForm.body) }}
5 | {{ form_end(articleForm) }}
6 | {# End Main Article Input Form #}
--------------------------------------------------------------------------------
/app/Resources/views/forms/contact.html.twig:
--------------------------------------------------------------------------------
1 | {# Begin Contact Us Form #}
2 | {{ form_start(contactForm, {'name': 'contactForm'}) }}
3 |
4 | {{ form_end(contactForm) }}
5 |
6 | {# End Contact Us Form #}
--------------------------------------------------------------------------------
/app/Resources/views/forms/email-report-success.html.twig:
--------------------------------------------------------------------------------
1 | {{ successMessage }}
--------------------------------------------------------------------------------
/app/Resources/views/forms/email-report.html.twig:
--------------------------------------------------------------------------------
1 | {# Begin Email Report Form #}
2 | {{ form_start(emailReportForm) }}
3 |
4 | {{ form_end(emailReportForm) }}
5 |
6 | {# End Email Report Form #}
--------------------------------------------------------------------------------
/app/Resources/views/forms/subscriber.html.twig:
--------------------------------------------------------------------------------
1 | {# Begin Subscriber Form #}
2 | {{ form(subscriberForm, { attr: {'id': 'subscriberForm'}}) }}
3 |
4 | {{ form_widget(subscriberForm.address) }}
5 |
6 |
7 | Subscribe
8 | No thanks - just optimize!
9 |
10 | {# End Subscriber Form #}
--------------------------------------------------------------------------------
/app/Resources/views/snippets/flickr-ajax-snippet.html.twig:
--------------------------------------------------------------------------------
1 | {# Closure to run on pageload to make ajax requests #}
2 | {# to backend for flickr images based on the top concepts #}
3 |
--------------------------------------------------------------------------------
/app/Resources/views/snippets/flickr-images.html.twig:
--------------------------------------------------------------------------------
1 | {# format: http://farm{farm-id}.static.flickr.com/{server-id}/{id}_{secret}.jpg #}
2 |
3 |
4 | {% for img in photos %}
5 |
6 |
7 |
8 |
9 | Click to Copy
10 |
11 |
12 |
13 |
14 | {% endfor %}
15 |
16 |
--------------------------------------------------------------------------------
/app/Resources/views/snippets/loading-animation.html.twig:
--------------------------------------------------------------------------------
1 | {# Begin Loading Overlay Animation #}
2 |
3 |
Analyzing...Please Wait...
4 |
5 |
6 | {# End Loading Overlay Animation #}
--------------------------------------------------------------------------------
/app/Resources/views/snippets/pinterest.html.twig:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/autoload.php:
--------------------------------------------------------------------------------
1 | tag that will appear in the tab
63 | app_title: "SEO Content Optimizer"
64 | ######################
65 | # Contact Requests
66 | ######################
67 | # The subject line that will appear on notification emails
68 | # when users submit feedback via the Contact form
69 | contact_email_subject: "Contact Request - Article Optimizer"
70 | # The email address that contact requests should be sent to
71 | # If you are the sole administrator of this application,
72 | # then this should be set to the email address at which
73 | # you want to receive user feedback, requests, complaints, etc.
74 | contact_email_delivery_address: "zackproser@gmail.com"
75 | # The customizable message that will appear to users
76 | # after they successfully submit feedback to you via
77 | # the contact form
78 | contact_success_response: "Thanks! We'll be in touch"
79 | #######################
80 | # Report Emailing
81 | #######################
82 | # The customizable subject line that will appear to the end recipient
83 | # any time that a user of this app sends a processed article analysis report
84 | # to an external user via the "Email this Report" functionality
85 | report_email_subject: "You've received an article optimization report"
86 | # The customizable message that will be displayed to the user
87 | # after they successfully submit a contact message to the app
88 | report_email_success_message: "Success! Your report was sent."
89 | #######################
90 | # Alchemy API
91 | #######################
92 | # Alchemy provides semantic analysis functions
93 | # Obtain your api key from www.alchemyapi.com
94 | alchemy_api_key: 'zgdhgg299999a0d5e93ef7518efr4f36s'
95 | # Alchemy API base URL:
96 | alchemy_api_base_url: https://gateway-a.watsonplatform.net/calls
97 | # Limit number of named entities to extract - defaults to 10
98 | alchemy_max_entities_retrieve: 15
99 | # Whether or not alchemy responses should include sentiment analysis - defaults to 1 (true)
100 | alchemy_include_sentiment: 1
101 | #######################
102 | # Bitly
103 | #######################
104 | # Bitly shortens URLs and provides metrics on its shortlinks
105 | # Obtain your token from www.bitly.com
106 | bitly_token: 'splrtrsdfh698288bf67999334'
107 | #######################
108 | # Flickr API
109 | #######################
110 | # Flickr provides photos to be used with articles
111 | flickr_api_key: 'zskleid44af23b666664c63'
112 | #######################
113 | # Article Reports
114 | #######################
115 | # The directory containing all of the badges for 100% optimized articles
116 | optimal_badge_directory: 100-percent-optimized
117 | # The directory containing all of the category badges (sub-optimal)
118 | category_badge_directory: categories
119 | ########################
120 | # Advertising
121 | ########################
122 | # Whether or not advertisement blocks should be rendered, using the following data
123 | ads_enabled: true
124 | advertisements:
125 | - cta: Download Perfect Wordpress Themes for 48 cents each
126 | img_path: ads/elegantthemes/300x250.gif
127 | url: http://www.elegantthemes.com/affiliates/idevaffiliate.php?id=9762_0_1_10
128 | - cta: Host your own article site and keep all the revenue
129 | img_path: ads/bluehost/300x250/bh_300x250_02.gif
130 | url: http://www.bluehost.com/track/zackproser/CODE43
131 | - cta: Automatically find keywords that earn money
132 | img_path: ads/MarketSamuraiBanner.jpg
133 | url: http://www.marketsamurai.com/c/articleoptimizer
134 | - cta: Outrank your competitors for competitive keywords
135 | img_path: ads/semrush_banner_300_250.png
136 | url: http://www.semrush.com/sem.html?ref=487183848
--------------------------------------------------------------------------------
/app/config/routing.yml:
--------------------------------------------------------------------------------
1 | _welcome:
2 | path: /
3 | defaults: { _controller: AppBundle:Default:index }
4 |
5 | article_process:
6 | path: /report
7 | defaults: { _controller: AppBundle:Default:articleForm }
8 |
9 | flickr_serve:
10 | path: /flickr-serve
11 | defaults: { _controller: Flickr:searchAction }
12 |
13 | bitly_shorten:
14 | path: /bitly-shorten
15 | defaults: { _controller: Bitly:shortenUrl }
16 |
17 | subscribe_process:
18 | path: /subscribe
19 | defaults: { _controller: AppBundle:Default:subscribe }
20 |
21 | contact_process:
22 | path: /contact
23 | defaults: { _controller: AppBundle:Default:contactForm }
24 |
25 | email_report_process:
26 | path: /email-report
27 | defaults: { _controller: AppBundle:Default:emailReport }
28 |
29 |
--------------------------------------------------------------------------------
/app/config/routing_dev.yml:
--------------------------------------------------------------------------------
1 | _wdt:
2 | resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml"
3 | prefix: /_wdt
4 |
5 | _profiler:
6 | resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml"
7 | prefix: /_profiler
8 |
9 | _errors:
10 | resource: "@TwigBundle/Resources/config/routing/errors.xml"
11 | prefix: /_error
12 |
13 | _main:
14 | resource: routing.yml
15 |
--------------------------------------------------------------------------------
/app/config/security.yml:
--------------------------------------------------------------------------------
1 | # To get started with security, check out the documentation:
2 | # http://symfony.com/doc/current/book/security.html
3 | security:
4 |
5 | # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
6 | providers:
7 | in_memory:
8 | memory: ~
9 |
10 | firewalls:
11 | # disables authentication for assets and the profiler, adapt it according to your needs
12 | dev:
13 | pattern: ^/(_(profiler|wdt)|css|images|js)/
14 | security: false
15 |
16 | main:
17 | anonymous: ~
18 | # activate different ways to authenticate
19 |
20 | # http_basic: ~
21 | # http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate
22 |
23 | # form_login: ~
24 | # http://symfony.com/doc/current/cookbook/security/form_login_setup.html
25 |
--------------------------------------------------------------------------------
/app/config/services.yml:
--------------------------------------------------------------------------------
1 | parameters:
2 | #parameter: value
3 | services:
4 | analyzer:
5 | class: AppBundle\Classes\Analyzer
6 | arguments: ["%alchemy_api_key%", "@curl", "@logger", "@service_container"]
7 | analysis_helper:
8 | class: AppBundle\Classes\AnalysisHelper
9 | arguments: ["@logger", "@service_container"]
10 | curl:
11 | class: AppBundle\Classes\CurlHelper
12 | arguments: ["%curl_timeout%", "@logger"]
13 | flickr:
14 | class: AppBundle\Classes\Flickr
15 | arguments: ["@service_container", "%flickr_api_key%", "@curl", "@logger"]
16 | bitly:
17 | class: AppBundle\Classes\Bitly
18 | arguments: ["%hostname%", "%bitly_token%", "@curl", "@logger"]
19 | app.twig_extension:
20 | class: AppBundle\Twig\AppExtension
21 | public: false
22 | tags:
23 | - { name: twig.extension }
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | getParameterOption(['--env', '-e'], getenv('SYMFONY_ENV') ?: 'dev');
23 | $debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(['--no-debug', '']) && $env !== 'prod';
24 |
25 | if ($debug) {
26 | Debug::enable();
27 | }
28 |
29 | $kernel = new AppKernel($env, $debug);
30 | $application = new Application($kernel);
31 | $application->run($input);
32 |
--------------------------------------------------------------------------------
/bin/symfony_requirements:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | getPhpIniConfigPath();
9 |
10 | echo_title('Symfony Requirements Checker');
11 |
12 | echo '> PHP is using the following php.ini file:'.PHP_EOL;
13 | if ($iniPath) {
14 | echo_style('green', ' '.$iniPath);
15 | } else {
16 | echo_style('warning', ' WARNING: No configuration file (php.ini) used by PHP!');
17 | }
18 |
19 | echo PHP_EOL.PHP_EOL;
20 |
21 | echo '> Checking Symfony requirements:'.PHP_EOL.' ';
22 |
23 | $messages = array();
24 | foreach ($symfonyRequirements->getRequirements() as $req) {
25 | /** @var $req Requirement */
26 | if ($helpText = get_error_message($req, $lineSize)) {
27 | echo_style('red', 'E');
28 | $messages['error'][] = $helpText;
29 | } else {
30 | echo_style('green', '.');
31 | }
32 | }
33 |
34 | $checkPassed = empty($messages['error']);
35 |
36 | foreach ($symfonyRequirements->getRecommendations() as $req) {
37 | if ($helpText = get_error_message($req, $lineSize)) {
38 | echo_style('yellow', 'W');
39 | $messages['warning'][] = $helpText;
40 | } else {
41 | echo_style('green', '.');
42 | }
43 | }
44 |
45 | if ($checkPassed) {
46 | echo_block('success', 'OK', 'Your system is ready to run Symfony projects');
47 | } else {
48 | echo_block('error', 'ERROR', 'Your system is not ready to run Symfony projects');
49 |
50 | echo_title('Fix the following mandatory requirements', 'red');
51 |
52 | foreach ($messages['error'] as $helpText) {
53 | echo ' * '.$helpText.PHP_EOL;
54 | }
55 | }
56 |
57 | if (!empty($messages['warning'])) {
58 | echo_title('Optional recommendations to improve your setup', 'yellow');
59 |
60 | foreach ($messages['warning'] as $helpText) {
61 | echo ' * '.$helpText.PHP_EOL;
62 | }
63 | }
64 |
65 | echo PHP_EOL;
66 | echo_style('title', 'Note');
67 | echo ' The command console could use a different php.ini file'.PHP_EOL;
68 | echo_style('title', '~~~~');
69 | echo ' than the one used with your web server. To be on the'.PHP_EOL;
70 | echo ' safe side, please check the requirements from your web'.PHP_EOL;
71 | echo ' server using the ';
72 | echo_style('yellow', 'web/config.php');
73 | echo ' script.'.PHP_EOL;
74 | echo PHP_EOL;
75 |
76 | exit($checkPassed ? 0 : 1);
77 |
78 | function get_error_message(Requirement $requirement, $lineSize)
79 | {
80 | if ($requirement->isFulfilled()) {
81 | return;
82 | }
83 |
84 | $errorMessage = wordwrap($requirement->getTestMessage(), $lineSize - 3, PHP_EOL.' ').PHP_EOL;
85 | $errorMessage .= ' > '.wordwrap($requirement->getHelpText(), $lineSize - 5, PHP_EOL.' > ').PHP_EOL;
86 |
87 | return $errorMessage;
88 | }
89 |
90 | function echo_title($title, $style = null)
91 | {
92 | $style = $style ?: 'title';
93 |
94 | echo PHP_EOL;
95 | echo_style($style, $title.PHP_EOL);
96 | echo_style($style, str_repeat('~', strlen($title)).PHP_EOL);
97 | echo PHP_EOL;
98 | }
99 |
100 | function echo_style($style, $message)
101 | {
102 | // ANSI color codes
103 | $styles = array(
104 | 'reset' => "\033[0m",
105 | 'red' => "\033[31m",
106 | 'green' => "\033[32m",
107 | 'yellow' => "\033[33m",
108 | 'error' => "\033[37;41m",
109 | 'success' => "\033[37;42m",
110 | 'title' => "\033[34m",
111 | );
112 | $supports = has_color_support();
113 |
114 | echo($supports ? $styles[$style] : '').$message.($supports ? $styles['reset'] : '');
115 | }
116 |
117 | function echo_block($style, $title, $message)
118 | {
119 | $message = ' '.trim($message).' ';
120 | $width = strlen($message);
121 |
122 | echo PHP_EOL.PHP_EOL;
123 |
124 | echo_style($style, str_repeat(' ', $width).PHP_EOL);
125 | echo_style($style, str_pad(' ['.$title.']', $width, ' ', STR_PAD_RIGHT).PHP_EOL);
126 | echo_style($style, str_pad($message, $width, ' ', STR_PAD_RIGHT).PHP_EOL);
127 | echo_style($style, str_repeat(' ', $width).PHP_EOL);
128 | }
129 |
130 | function has_color_support()
131 | {
132 | static $support;
133 |
134 | if (null === $support) {
135 | if (DIRECTORY_SEPARATOR == '\\') {
136 | $support = false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI');
137 | } else {
138 | $support = function_exists('posix_isatty') && @posix_isatty(STDOUT);
139 | }
140 | }
141 |
142 | return $support;
143 | }
144 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "arcanesanctum/articleoptimizer",
3 | "license": "proprietary",
4 | "type": "project",
5 | "autoload": {
6 | "psr-4": {
7 | "": "src/"
8 | },
9 | "classmap": [
10 | "app/AppKernel.php",
11 | "app/AppCache.php"
12 | ]
13 | },
14 | "autoload-dev": {
15 | "psr-4": {
16 | "Tests\\": "tests/"
17 | }
18 | },
19 | "require": {
20 | "php": ">=5.5.9",
21 | "symfony/symfony": "3.1.*",
22 | "doctrine/orm": "^2.5",
23 | "doctrine/doctrine-bundle": "^1.6",
24 | "doctrine/doctrine-cache-bundle": "^1.2",
25 | "symfony/swiftmailer-bundle": "^2.3",
26 | "symfony/monolog-bundle": "^2.8",
27 | "symfony/polyfill-apcu": "^1.0",
28 | "sensio/distribution-bundle": "^5.0",
29 | "sensio/framework-extra-bundle": "^3.0.2",
30 | "incenteev/composer-parameter-handler": "^2.0",
31 | "symfony/assetic-bundle": "2.8",
32 | "justinrainbow/json-schema": "~2.0"
33 | },
34 | "require-dev": {
35 | "sensio/generator-bundle": "^3.0",
36 | "symfony/phpunit-bridge": "^3.0"
37 | },
38 | "scripts": {
39 | "post-install-cmd": [
40 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters",
41 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
42 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
43 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets",
44 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile",
45 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget"
46 | ],
47 | "post-update-cmd": [
48 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters",
49 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
50 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
51 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets",
52 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile",
53 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget"
54 | ]
55 | },
56 | "extra": {
57 | "symfony-app-dir": "app",
58 | "symfony-bin-dir": "bin",
59 | "symfony-var-dir": "var",
60 | "symfony-web-dir": "web",
61 | "symfony-tests-dir": "tests",
62 | "symfony-assets-install": "relative",
63 | "incenteev-parameters": {
64 | "file": "app/config/parameters.yml"
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/doc/img/symfony-optimizer-splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/doc/img/symfony-optimizer-splash.png
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | tests
18 |
19 |
20 |
21 |
22 |
23 | src
24 |
25 | src/*Bundle/Resources
26 | src/*/*Bundle/Resources
27 | src/*/Bundle/*Bundle/Resources
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/runTests.sh:
--------------------------------------------------------------------------------
1 | phpunit;
--------------------------------------------------------------------------------
/src/.htaccess:
--------------------------------------------------------------------------------
1 |
2 | Require all denied
3 |
4 |
5 | Order deny,allow
6 | Deny from all
7 |
8 |
--------------------------------------------------------------------------------
/src/AppBundle/AppBundle.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
20 |
21 | $this->container = $container;
22 | }
23 |
24 | /**
25 | * Sanitize and return the article body
26 | *
27 | * @param String $articleText The text of the article
28 | * @return String $articleText The santitized text of the article
29 | */
30 | public function sanitizeArticleBody($articleText)
31 | {
32 | //Convert all html entities to their applicable characters - e.g.
33 | $articleText = html_entity_decode($articleText);
34 |
35 | //Clean submitted article body of any HTML or PHP tags
36 | $articleText = strip_tags($articleText);
37 |
38 | //Drop all non-alphanumeric characters, but preserve single spaces
39 | $articleText = preg_replace('/[^A-Za-z0-9\-\s]/', '', $articleText);
40 |
41 | //Replace all hyphens with spaces
42 | $articleText = preg_replace('/\-/', ' ', $articleText);
43 |
44 | return $articleText;
45 | }
46 |
47 | /**
48 | * Parses the Alchemy API response and stores the data in the analysis array
49 | *
50 | * Modifes the analysis array in-place
51 | *
52 | * @param Array $alchemyResponse The response from Alchemy API after processing the article
53 | * @param Array &$analysis The current analysis
54 | */
55 | public function parseAlchemyResponse($alchemyResponse, &$analysis)
56 | {
57 | //Decode Alchemy's response JSON into an associative array
58 | try {
59 | $alchemyResponse = json_decode($alchemyResponse, true);
60 | } catch (\Exception $e) {
61 | //TODO: handle this
62 | $this->logger->error('Error decoding Alchemy API response JSON: ' . $alchemyResponse);
63 | }
64 |
65 | $analysis['flickrKeywords'] = array();
66 |
67 | //The properties of a typical Alchemy API response that we are interested in
68 | $props = array(
69 | 'keywords',
70 | 'concepts',
71 | 'entities',
72 | 'taxonomy',
73 | 'category'
74 | );
75 | //Loop through and inspect each of these properties in the Alchemy API response
76 | //and store them in analysis if they are set
77 | if (isset($alchemyResponse['status']) && $alchemyResponse['status'] === 'OK') {
78 |
79 | foreach($props as $prop) {
80 | //Initialize success/failure key in the analysis array for each property
81 | $analysis[$prop .'Succeeded'] = false;
82 |
83 | if (isset($alchemyResponse[$prop]) && count($alchemyResponse[$prop]) > 0) {
84 | //var_dump($alchemyResponse[$prop]);
85 | //If the given property is set and has at least one entry, save it to analysis
86 | $analysis[$prop] = $alchemyResponse[$prop];
87 | //Skim a sampling of all properties that aren't taxonomy to build
88 | //a diverse and accurate sampling of topics for searching via Flickr
89 | if ($prop != 'taxonomy' && isset($analysis[$prop][0]['text'])) {
90 | $analysis['flickrKeywords'][] = strtolower($analysis[$prop][0]['text']);
91 | }
92 | $analysis[$prop . 'Succeeded'] = true;
93 | }
94 | }
95 |
96 | //Parse the Alchemy taxonomy information to extract a generic article category
97 | //if possible. Otherwise, fall back to a generic category name 'General Interest'
98 | $this->parseAlchemyTaxonomy($analysis);
99 |
100 | } else {
101 | $this->logger->error('Received Alchemy API failure: ' . $alchemyResponse['status'] ?? '');
102 | }
103 | }
104 |
105 | /**
106 | * Examines Alchemy API assigned taxonomy information
107 | *
108 | * Extracts the most descriptive root taxonomy and formats it so that
109 | * it is appropriate for displaying to the end user
110 | *
111 | * Falls back to generic "General Interest" should processsing the taxonomy fail
112 | *
113 | * Modifies the analysis array in-place
114 | *
115 | * @param Array &$analysis The associative array representing all analysis completed so far
116 | */
117 | private function parseAlchemyTaxonomy(&$analysis)
118 | {
119 | if (isset($analysis['taxonomy']) && count($analysis['taxonomy']) > 0 && isset($analysis['taxonomy'][0]['label'])) {
120 | $categories = explode('/', $analysis['taxonomy'][0]['label']);
121 | $analysis['category'] = (isset($categories[1]) && gettype($categories[1] === 'string')) ? ucwords($categories[1]) : 'General Interest';
122 | $analysis['categorySucceeded'] = true;
123 | }
124 | }
125 |
126 | /**
127 | * Maps the Alchemy-provided taxonomy to a
128 | * category badge directory.
129 | *
130 | * Category badges are 'consolation' badges
131 | * specific to the content categroy of a given
132 | * article which was not totally optimized
133 | *
134 | * Totally optimized articles receive a
135 | * completely different type of badge
136 | *
137 | * @param String $category The Alchemy-provided
138 | * @return String $directory The name of the directory from which a badge should be selected
139 | */
140 | public function mapCategoryToBadge($category)
141 | {
142 | switch($category) {
143 | case 'Art And Entertainment':
144 | case 'Education':
145 | return 'arts-entertainment';
146 | case 'Health And Fitness':
147 | return 'health';
148 | case 'Hobbies And Interests':
149 | case 'Automotive':
150 | case 'Home And Garden':
151 | return 'recreation';
152 | case 'Business':
153 | case 'Careers':
154 | return 'business';
155 | case 'Family And Parenting':
156 | case 'Law, Govt And Politics':
157 | case 'Style And Fashion':
158 | return 'culture-politics';
159 | case 'Health And Fitness':
160 | case 'Food And Drink':
161 | return 'health';
162 | case 'General Interest':
163 | default:
164 | return 'unknown';
165 | }
166 | }
167 |
168 | /**
169 | * Generates a unique path for a given report
170 | *
171 | * @return String - the relative path where this report will be saved
172 | */
173 | public function generateReportUrl()
174 | {
175 | $today = date('M-d-o-g-i-s');
176 | $random_token = md5(uniqid(rand(), true));
177 | return 'saved-reports/'.$today.'-'.$random_token.'.html';
178 | }
179 |
180 | /**
181 | * Subscribe a user to the configured mailchimp list associated with this app
182 | *
183 | * @param String $emailAddress The email address to subscribe
184 | */
185 | public function subscribe($emailAddress) {
186 |
187 | //Mailchimp's base API url - N.B: you must include the correct
datacenter for your account
188 | //e.g: us7 or us2
189 | $mailchimpAPIRoot = $this->container->getParameter('mailchimp_api_root');
190 | $listId = $this->container->getParameter('mailchimp_list_id');
191 | //Mailchimp expects user email addresses to be hashed with the md5 algorithm
192 | $hashedEmailAddress = md5($emailAddress);
193 |
194 | //Build URL for making the subscription request to Mailchimp
195 | $subscribeUserUrl = sprintf("%slists/%s/members", $mailchimpAPIRoot, $listId);
196 |
197 | $payload = array();
198 |
199 | $payload['email_address'] = $emailAddress;
200 | //Tell mailchimp we want them to send a confirmation email to the user's address
201 | //This will ultimately increase list quality and reduce fraud and abuse
202 | $payload['status'] = 'pending';
203 |
204 | $headers = array();
205 |
206 | //Mailchimp expects you to supply your API key via HTTP Basic Auth
207 | //@See: http://developer.mailchimp.com/documentation/mailchimp/guides/manage-subscribers-with-the-mailchimp-api
208 | $options = array(
209 | 'CURLOPT_USERPWD' => 'articleoptimizer:' . $this->container->getParameter('mailchimp_api_key'),
210 | 'CURLOPT_HTTPAUTH' => CURLAUTH_BASIC
211 | );
212 |
213 | return $this->container->get('curl')->post($subscribeUserUrl, json_encode($payload), $headers, $options);
214 | }
215 | }
216 |
217 | ?>
--------------------------------------------------------------------------------
/src/AppBundle/Classes/Analyzer.php:
--------------------------------------------------------------------------------
1 | alchemyAPIKey = $alchemyApiKey;
36 |
37 | //Set up the logger
38 | $this->logger = $logger;
39 |
40 | //Curl helper service
41 | $this->curl = $curl;
42 |
43 | //Store a reference to the service container
44 | $this->container = $container;
45 |
46 | //Analysis Helper is a class that contains helper functions used by this Analyzer
47 | $this->analysisHelper = $this->container->get('analysis_helper');
48 |
49 | //Set the names of the optimized and category badge directories
50 | $this->optimizedBadgeDirectory = $container->getParameter('optimal_badge_directory');
51 |
52 | $this->categoryBadgeDirectory = $container->getParameter('category_badge_directory');
53 | }
54 |
55 | /**
56 | * Perform analysis functions on article text
57 | *
58 | * As well as pass text to Alchemy API for linguistic analysis
59 | *
60 | * Builds up an associative array containing all analaysis information
61 | *
62 | * To be rendered on the report
63 | *
64 | * @param Article $article the body of the article that should be analyzed
65 | * @return Analysis array the associative array containing all analysis information
66 | */
67 | public function analyze(Article $article)
68 | {
69 | //Main array that will contain all analysis data
70 | $analysis = array(
71 | 'success' => false,
72 | 'wordCount' => 0
73 | );
74 |
75 | //Options to pass to Alchemy API: drop unnecessary metadata about decision sources to speed up processing
76 | $options = array(
77 | //'linkedData' => 1
78 | );
79 |
80 | //Pull the article body off the article object
81 | $articleText = $article->getBody();
82 |
83 | //Store the text in the analysis for later rendering on the report next to the badge
84 | $analysis['articleBody'] = $article->getBody();
85 |
86 | if (isset($articleText) && is_string($articleText) && strlen($articleText) > 1) {
87 | $analysis['wordCount'] = str_word_count($articleText);
88 | $analysis['success'] = true;
89 | } else {
90 | $analysis['success'] = false;
91 | $this->logger->info(sprintf('Invalid article of length: %s after sanitization', strlen($articleText)));
92 | }
93 |
94 | //Sanitize the article text
95 | $articleText = $this->analysisHelper->sanitizeArticleBody($articleText);
96 |
97 | //Prepare and execute request to Alchemy API
98 | $alchemyResponse = $this->alchemizeText($articleText, $analysis);
99 |
100 | //Validates and gathers required data from an Alchemy response
101 | //Formats response data neatly so it can be sanely accessed by twig
102 | $this->analysisHelper->parseAlchemyResponse($alchemyResponse, $analysis);
103 |
104 | //Make determinations on how often the key phrases were repeated throughout
105 | $this->analyzePhraseDensity($analysis);
106 |
107 | //Select the badge image that will be used for this report
108 | $this->selectReportBadge($analysis);
109 |
110 | //Generate advertisements
111 | $this->fetchAdvertisements($analysis);
112 |
113 | return $analysis;
114 | }
115 |
116 | /**
117 | * Prepares and executes request to Alchemy API for further processing
118 | *
119 | * @param String $articleText The sanitized article body to send to Alchemy
120 | */
121 | public function alchemizeText(String $articleText, &$analysis)
122 | {
123 | //Form the target URL to call, passing our Alchemy API key as a query string parameter
124 | $targetUrl = $this->container->getParameter('alchemy_api_base_url') . "/text/TextGetCombinedData?outputMode=json&apikey=" . $this->alchemyAPIKey;
125 |
126 | //Use the alchemy parameters if they are set, otherwise sensible defaults
127 | $data = sprintf("maxRetrieve=%d&sentiment=%d&text=%s",
128 | $this->container->getParameter('alchemy_max_entities_retrieve') ?? 10,
129 | $this->container->getParameter('alchemy_include_sentiment') ?? 1,
130 | urlencode($articleText)
131 | );
132 |
133 | //In order to be able to POST a body to Alchemy API, this header must be set exactly as so:
134 | $headers = array(
135 | 'Content-Type: application/x-www-form-urlencoded'
136 | );
137 |
138 | //Use curl service to POST request to Alchemy API
139 | $alchemyResponse = $this->curl->post($targetUrl, $data, $headers);
140 |
141 | //If we received a 200 from Alchemy, we have a successful analysis - otherwise we don't
142 | if (200 === $alchemyResponse->getStatusCode()) {
143 | return $alchemyResponse->getContent();
144 | } else {
145 | $analysis['success'] = false;
146 | $this->logger->error(sprintf("Error retrieving results from Alchemy API: %s and status code: %d", $alchemyResponse->getContent(), $alchemyResponse->getStatusCode()));
147 | return;
148 | }
149 | }
150 |
151 | /**
152 | * Fetches advertisement blocks defined in parameters.yml
153 | * for rendering in the report
154 | *
155 | * @param Array &$analysis The current analysis
156 | */
157 | private function fetchAdvertisements(&$analysis)
158 | {
159 | $analysis['adsEnabled'] = $this->container->getParameter('ads_enabled');
160 | if ($analysis['adsEnabled'] === true) {
161 | $analysis['ads'] = $this->container->getParameter('advertisements');
162 | }
163 | }
164 |
165 | /**
166 | * Makes determinations on the frequency with which key phrases are repeated
167 | * which are used to make recommendations on the report page
168 | *
169 | * Modifies the analysis array in place
170 | *
171 | * @param Array &$analysis The current analysis
172 | */
173 | private function analyzePhraseDensity(&$analysis)
174 | {
175 | if (isset($analysis['entitiesSucceeded']) && $analysis['entitiesSucceeded'] === true) {
176 | //Initialize density array
177 | $density = array(
178 | 'stuffing' => array(),
179 | 'good' => array(),
180 | 'low' => array()
181 | );
182 | foreach($analysis['entities'] as $entity) {
183 | $count = (int)$entity['count'];
184 | $entity = $entity['text'];
185 | $p = round((($count / $analysis['wordCount']) * 100), 2);
186 | switch(true) {
187 | case ($p >= 0.20 && $p <= 1.50):
188 | $density['good'][$entity] = $p;
189 | break;
190 | case ($p < 0.20):
191 | $density['low'][$entity] = $p;
192 | break;
193 | case ($p > 1.50):
194 | $density['stuffing'][$entity] = $p;
195 | break;
196 | default:
197 | $density['good'][$entity] = $p;
198 | break;
199 | }
200 | }
201 | $analysis['phraseDensitySucceeded'] = false;
202 | foreach($density as $level => $children) {
203 | if (is_array($level) && count($level) > 0) {
204 | $analysis['phraseDensitySucceeded'] = true;
205 | }
206 | }
207 | if ($analysis['phraseDensitySucceeded'] = true) {
208 | $analysis['phraseDensity'] = $density;
209 | }
210 | }
211 | }
212 |
213 | /**
214 | * Chooses a report badge based on the state of the analysis
215 | *
216 | * If the article is long enough and has at least one keyword
217 | * in the "good" range as determined by the analyzePhraseDensity
218 | * method, it receives an optimized badge
219 | *
220 | * Otherwise it receives a badge based on its category
221 | *
222 | * @param Array &$analysis The current analysis
223 | */
224 | private function selectReportBadge(&$analysis)
225 | {
226 | //If the article is greater than 151 words, that's one positive check toward its quality
227 | $wordCountCheck = ($analysis['wordCount'] >= 151) ? true : false;
228 | $keywordsCheck = (isset($analysis['phraseDensity']['good']) && count($analysis['phraseDensity']['good']) >= 1) ? true : false;
229 |
230 | $analysis['wordCountCheck'] = $wordCountCheck;
231 | $analysis['keywordsCheck'] = $keywordsCheck;
232 |
233 | if (true === $wordCountCheck && true === $keywordsCheck) {
234 |
235 | //We have a high quality article - render an optimized badge
236 | $analysis['badge'] = $this->getBadgeUrl('optimized', null);
237 | } else {
238 | //Use a category badge instead
239 | $badgeCategory = isset($analysis['category']) ? $this->analysisHelper->mapCategoryToBadge($analysis['category']) : 'unknown';
240 | $analysis['badge'] = $this->getBadgeUrl('category', $badgeCategory);
241 | }
242 | }
243 |
244 | /**
245 | * Builds the system path to the /web directory of this Symfony project
246 | *
247 | * @return string path - the system path to the /web directory
248 | */
249 | private function getWebDirectory()
250 | {
251 | return dirname($this->container->get('kernel')->getRootDir()) . '/web';
252 | }
253 |
254 | /**
255 | * Obtain an optimized or a category badge URL, depending on the mode passed to this method
256 | *
257 | * @var string mode - either 'optimized' or 'category' for specifying the type of badge that should be found
258 | *
259 | * @return string url - the relative URL to the badge that was selected for this report. Used by report.html.twig to display the badge
260 | */
261 | //private function getBadgeUrl($mode, string $category = null)
262 | private function getBadgeUrl($mode, $category = null)
263 | {
264 | $badges = array();
265 |
266 | //If an optimized badge is being requested, choose one at random from its directory
267 | //Category badges requring dropping down into the specific category directory before selecting a badge
268 | $pathRoot = '/img/badges/' . ($mode === 'optimized' ? $this->optimizedBadgeDirectory : $this->categoryBadgeDirectory . '/' . $category) . '/';
269 |
270 | //Instantiate Symfony file finder object
271 | $finder = new Finder();
272 | //Only pick up jpegs, (or .jpg), .png, .gif or .gifv files
273 | $finder->files()->name('/(:?.*\.jpe?g$|.*\.png$|.*\.gifv?$)/')->in($this->getWebDirectory() . $pathRoot);
274 |
275 | foreach ($finder as $file) {
276 | //Build the full path to the badge image and store it in the array of possible badges
277 | array_push($badges, ($pathRoot . $file->getRelativePathname()));
278 | }
279 |
280 | //Choose one of the optimized badges at random
281 | return $badges[array_rand($badges)];
282 | }
283 | }
284 |
285 | ?>
--------------------------------------------------------------------------------
/src/AppBundle/Classes/Bitly.php:
--------------------------------------------------------------------------------
1 | hostname = $hostname;
37 |
38 | $this->token = $token;
39 |
40 | $this->curl = $curl;
41 |
42 | $this->logger = $logger;
43 | }
44 |
45 | /**
46 | * Accepts a relative URL, builds it into a fully qualified URL, then shortens that link via Bitly
47 | *
48 | * @param string $url - The relative URL to the report that should be fully qualified and shortened
49 | * @return string $shortUrl - The fully qualified and shortened URL
50 | */
51 | public function shortenUrl(Request $request)
52 | {
53 | //Build the query parameters
54 | $queryParams = array(
55 | 'access_token' => $this->token,
56 | 'longUrl' => $request->get('url'),
57 | 'format' => 'json'
58 | );
59 |
60 | //Build the URL to make the Bitly API request
61 | $bitlyTarget = 'https://api-ssl.bitly.com/v3/shorten?' . http_build_query($queryParams);
62 |
63 | //Remove default urlencoding that http_build_query creates, as Bitly will choke on it
64 | $bitlyTarget = urldecode($bitlyTarget);
65 |
66 | $bitlyResponse = $this->curl->get($bitlyTarget);
67 |
68 | return $bitlyResponse;
69 | }
70 | }
--------------------------------------------------------------------------------
/src/AppBundle/Classes/CurlHelper.php:
--------------------------------------------------------------------------------
1 | timeout = $timeout;
24 |
25 | $this->logger = $logger;
26 | }
27 |
28 | public function get($url, $headers = array(), $options = array())
29 | {
30 | if (!isset($url)) {
31 | $this->logger->error('Bad url passed to curl get: ' . $url);
32 | throw new \Exception('Curl get requires a valid URL');
33 | }
34 |
35 | $ch = curl_init();
36 | $this->applyDefaultCurlSettings($ch, $url, null, $headers, $options);
37 |
38 | return $this->executeCurlRequest($ch);
39 | }
40 |
41 | public function post($url, $data, $headers = array(), $options = array())
42 | {
43 | if (!isset($url)) {
44 | $this->logger->error('Bad url passed to curl post: ' . $url);
45 | throw new \Exception('Curl post requires a valid URL');
46 | }
47 |
48 | $ch = curl_init();
49 | $this->applyDefaultCurlSettings($ch, $url, $data, $headers, $options);
50 |
51 | curl_setopt($ch, CURLOPT_POST, true);
52 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data ?? array());
53 |
54 | return $this->executeCurlRequest($ch);
55 | }
56 |
57 | private function applyDefaultCurlSettings(&$ch, $url, $data = null, $headers = array(), $options = null)
58 | {
59 | curl_setopt($ch, CURLOPT_HEADER, $options['CURLOPT_HEADER'] ?? false);
60 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers ?? array());
61 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, $options['CURLOPT_RETURNTRANSFER'] ?? true);
62 | curl_setopt($ch, CURLOPT_USERAGENT, $options['CURLOPT_USERAGENT'] ?? 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36');
63 | curl_setopt($ch, CURLOPT_URL, $url ?? $options['CURLOPT_URL']);
64 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, $options['CURLOPT_FOLLOWLOCATION'] ?? true);
65 | if (isset($options['CURLOPT_USERPWD'])) {
66 | curl_setopt($ch, CURLOPT_USERPWD, $options['CURLOPT_USERPWD']);
67 | }
68 | if (isset($options['CURLOPT_HTTPAUTH'])) {
69 | curl_setopt($ch, CURLOPT_HTTPAUTH, $options['CURLOPT_HTTPAUTH']);
70 | }
71 | }
72 |
73 | private function executeCurlRequest(&$ch)
74 | {
75 | $curlReqResult = curl_exec($ch);
76 | $curlReqHttpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
77 | curl_close($ch);
78 |
79 | $response = new Response();
80 | $response->setStatusCode($curlReqHttpCode);
81 | $response->setContent($curlReqResult);
82 |
83 | return $response;
84 | }
85 | }
86 |
87 | ?>
--------------------------------------------------------------------------------
/src/AppBundle/Classes/Flickr.php:
--------------------------------------------------------------------------------
1 | container = $container;
32 |
33 | $this->apiKey = $apiKey;
34 |
35 | $this->curl = $curl;
36 |
37 | $this->logger = $logger;
38 |
39 | $this->baseFlickrUrl = 'http://flickr.com/services/rest/?';
40 |
41 | }
42 |
43 | /**
44 | * Obtains copyright-free images from the Flickr API
45 | *
46 | * @param Request $request Symfony request
47 | * @return Response $response Symfony response
48 | */
49 | public function searchAction(Request $request)
50 | {
51 | //Pull the query off the search
52 | $postString = $request->getContent();
53 |
54 | //Pull the keywords leading string off
55 | $postString = str_replace('keywords=', '', $postString);
56 |
57 | $postString = urldecode($postString);
58 |
59 | //Form an array of search terms to loop through
60 | $searchTerms = explode(',', $postString);
61 |
62 | //Initialize array that will hold photos returned by Flickr
63 | $photos = array();
64 |
65 | foreach($searchTerms as $query) {
66 | //Build arguments array
67 | $arguments = array(
68 | 'method' => 'flickr.photos.search',
69 | 'api_key' => $this->apiKey,
70 | 'tags' => urlencode($query),
71 | 'text' => urlencode($query),
72 | 'per_page' => 50,
73 | 'content_type' => 1, //Photos only - not screenshots which tend to be lower quality
74 | 'format' => 'php_serial'
75 | );
76 |
77 | //Build full Flickr API request URL
78 | $searchUrl = $this->baseFlickrUrl.http_build_query($arguments);
79 |
80 | //Make Flickr API request via curl helper
81 | $flickrResponse = $this->curl->get($searchUrl);
82 |
83 | if ($flickrResponse->getStatusCode() === 200) {
84 | $data = unserialize($flickrResponse->getContent());
85 | if (isset($data['photos']['photo'])) {
86 | array_push($photos, $data['photos']['photo']);
87 | }
88 | }
89 | }
90 | //Return rendered html containing the images
91 | //flickr-images.html.twig creates correct URLs from
92 | //data returned by the above Flickr API calls
93 | return $this->render('snippets/flickr-images.html.twig', array(
94 | 'photos' => isset($data['photos']['photo']) ? $data['photos']['photo'] : null
95 | ));
96 | }
97 | }
98 |
99 |
100 | ?>
--------------------------------------------------------------------------------
/src/AppBundle/Controller/DefaultController.php:
--------------------------------------------------------------------------------
1 | getUserCookieStatus($request);
38 | //Build and return response, including all the app-level configuration params
39 | //That will be used to render owner-specific data in meta tags, title, etc.
40 | $response = $this->render('default/index.html.twig', array(
41 | 'returningUser' => $userStatus,
42 | 'appTitle' => $this->getParameter('app_title'),
43 | 'appAuthor' => $this->getParameter('app_author')
44 | ));
45 |
46 | return $response;
47 | }
48 |
49 | /**
50 | * Render the form that accepts articles
51 | *
52 | * @param Request $request - the Symfony request
53 | * @return Response $response - the Symfony response
54 | */
55 | public function articleFormAction(Request $request)
56 | {
57 | //Create a new article entity and its accompanying form
58 | $article = new Article();
59 | //Set the createdDate to the time of the request
60 | $article->setCreatedDate(new \DateTime());
61 |
62 | $articleForm = $this->createFormBuilder($article)
63 | ->setAction($this->generateUrl('article_process'))
64 | ->add('body', TextareaType::class, array(
65 | 'label' => ' ',
66 | 'attr' => array('class' => 'form-control article-form',
67 | 'placeholder' => 'Step 1: Paste Your Article Here'
68 | )
69 | )
70 | )
71 | ->add('submit', SubmitType::class, array(
72 | 'label' => 'Step 2: Click Here to Optimize!',
73 | 'attr' => array(
74 | 'class' => 'submit-button article-submit btn btn-success'
75 | )
76 | )
77 | )
78 | ->getForm();
79 |
80 | $articleForm->handleRequest($request);
81 |
82 | if ($articleForm->isSubmitted() && $articleForm->isValid()) {
83 |
84 | //Load Analyzer service
85 | $analyzer = $this->get('analyzer');
86 |
87 | //Obtain analysis of the article
88 | $analysis = $analyzer->analyze($article);
89 |
90 | //Analysis helper builds the final destination path for the report
91 | $reportDestination = $this->container->get('analysis_helper')->generateReportUrl();
92 |
93 | //First, render the report and write to the server for later use
94 | $renderedReport = $this->renderView('default/report.html.twig', array(
95 | 'analysis' => $analysis,
96 | 'appTitle' => $this->getParameter('app_title'),
97 | 'appAuthor' => $this->getParameter('app_author')
98 | )
99 | );
100 |
101 | //Instantiate new Symfony filesystem component
102 | $fs = new Filesystem();
103 |
104 | try {
105 | //Write the rendered repport to the file for later access
106 | $fs->dumpFile($reportDestination, $renderedReport);
107 |
108 | } catch (\Exception $e) {
109 | $this->container->get('logger')->error('Error persisting generated report to filesystem: ' . $e);
110 | }
111 |
112 | //Create a new redirect response to the newly written report
113 | $response = new RedirectResponse($reportDestination, 301);
114 | //Set the returning user cookie in the response so that user
115 | //will not be required to pass sign-up flow again
116 | $this->setUserCookie($response);
117 |
118 | //Redirect request to the written report
119 | return $response;
120 |
121 | }
122 |
123 | //Render the article form
124 | return $this->render('forms/article.html.twig', array(
125 | 'articleForm' => $articleForm->createView()
126 | )
127 | );
128 | }
129 |
130 | /**
131 | * Render the contact request form and process its submission
132 | *
133 | * Sends a contact request email using the configured mailer on
134 | *
135 | * Submission of a valid contact request form
136 | *
137 | * @param Request $request The request
138 | * @return Response $response The response
139 | */
140 | public function contactFormAction(Request $request)
141 | {
142 | //Build the contact us form
143 | $contact = new ContactRequest();
144 |
145 | $contactForm = $this->createFormBuilder($contact)
146 | ->setAction($this->generateUrl('contact_process'))
147 | ->add('body', TextareaType::class, array(
148 | 'label' => ' ',
149 | 'attr' => array('class' => 'form-control contact-form',
150 | 'placeholder' => 'What\'s on your mind?'))
151 | )
152 | ->add('requestor', TextType::class, array(
153 | 'label' => ' ',
154 | 'attr' => array('class' => 'form-control contact-email',
155 | 'placeholder' => 'Optionally, add your email address here if you want a response'))
156 | )
157 | ->add('submitContact', SubmitType::class, array(
158 | 'label' => 'Submit Feedback',
159 | 'attr' => array(
160 | 'class' => 'submit-button btn btn-success')))
161 | ->getForm();
162 |
163 | $contactForm->handleRequest($request);
164 |
165 | if ($contactForm->isSubmitted() && $contactForm->isValid()) {
166 |
167 | //Set the createdDate to the time of the valid submission
168 | $contact->setCreatedDate(new \DateTime());
169 |
170 | //Get the Requestor off the contact record - if it exists
171 | $requestorField = $contact->getRequestor();
172 | $requestor = ($requestorField ?? "anonymous@article-optimize.com");
173 |
174 | $message = $contact->getBody();
175 |
176 | //Prepare the contact request email
177 | $contactEmail = \Swift_Message::newInstance()
178 | ->setSubject($this->getParameter('contact_email_subject'))
179 | ->setFrom($requestor)
180 | ->setTo($this->getParameter('contact_email_delivery_address'))
181 | ->setBody(
182 | $this->renderView(
183 | 'emails/contact.html.twig',
184 | array('requestor' => $requestor,
185 | 'message' => $message
186 | ),
187 | 'text/html'
188 | )
189 | )
190 | ;
191 | //Send the contact request email
192 | $this->get('mailer')->send($contactEmail);
193 |
194 | //Render the contact success page with customized message
195 | return $this->render('default/contact-success.html.twig', array(
196 | 'contact_success_response' => $this->getParameter('contact_success_response')
197 | )
198 | );
199 | }
200 |
201 | //Don't return full form view for ajax requests
202 | if (!$request->isXmlHttpRequest()) {
203 | return $this->render('forms/contact.html.twig', array(
204 | 'contactForm' => $contactForm->createView(),
205 | ));
206 | } else {
207 |
208 | $response = new Response();
209 | $response->setStatusCode(400);
210 | $response->setContent($this->getFormErrorsAsString($contactForm));
211 | return $response;
212 | }
213 | }
214 |
215 | /**
216 | * Sends a report via email to a recipient specified by a user
217 | *
218 | * @param Request Symfony request
219 | * @return Response Symfony response
220 | */
221 | public function emailReportAction(Request $request)
222 | {
223 | $recipient = new ReportEmailRecipient();
224 |
225 | $recipient->setReportUrl($request->headers->get('referer'));
226 |
227 | $recipientForm = $this->createFormBuilder($recipient, array(), array())
228 | ->setAction($this->generateUrl('email_report_process'))
229 | ->add('address', TextType::class, array(
230 | 'label' => ' ',
231 | 'attr' => array('class' => 'form-control report-email',
232 | 'placeholder' => 'Enter a valid email address to send this report to'))
233 | )
234 | ->add('sendReport', SubmitType::class, array(
235 | 'label' => 'Send Report',
236 | 'attr' => array(
237 | 'class' => 'submit-button report-send-button btn btn-success'))
238 | )
239 | ->getForm();
240 |
241 | $recipientForm->handleRequest($request);
242 |
243 | if ($recipientForm->isSubmitted() && $recipientForm->isValid()) {
244 |
245 | //Prepare the report-sending email
246 | $recipientEmail = \Swift_Message::newInstance()
247 | ->setSubject($this->getParameter('report_email_subject'))
248 | ->setFrom('report@article-optimize.com')
249 | ->setTo($recipient->getAddress())
250 | ->setBody(
251 | $this->renderView(
252 | 'emails/report.html.twig',
253 | array('reportUrl' => $request->get('uri'))
254 | ),
255 | 'text/html'
256 | )
257 | ;
258 | //Send the contact request email
259 | $this->get('mailer')->send($recipientEmail);
260 |
261 | //Render the contact success page with customized message
262 | return $this->render('forms/email-report-success.html.twig', array(
263 | 'successMessage' => $this->getParameter('report_email_success_message')
264 | )
265 | );
266 | }
267 |
268 | return $this->render('forms/email-report.html.twig', array(
269 | 'emailReportForm' => $recipientForm->createView(),
270 | )
271 | );
272 | }
273 |
274 | /**
275 | * Handles user subscriptions to email list via signup modal on index page
276 | *
277 | * @param Request $request Symfony request
278 | */
279 | public function subscribeAction(Request $request)
280 | {
281 | $subscriber = new Subscriber();
282 |
283 | $subscriberForm = $this->createFormBuilder($subscriber)
284 | ->setAction($this->generateUrl('subscribe_process'))
285 | ->add('address', TextType::class, array(
286 | 'label' => ' ',
287 | 'attr' => array('class' => 'form-control report-email',
288 | 'placeholder' => 'Enter a valid email address to subscribe'))
289 | )
290 | ->getForm();
291 |
292 | $subscriberForm->handleRequest($request);
293 |
294 | if ($subscriberForm->isSubmitted() && $subscriberForm->isValid()) {
295 |
296 | $mailchimpResponse = $this->get('analysis_helper')->subscribe($subscriber->getAddress());
297 |
298 | $mailchimpStatusCode = $mailchimpResponse->getStatusCode();
299 |
300 | if (200 === $mailchimpStatusCode) {
301 | $message = 'Success! You\'ve been subscribed. Please check your email to confirm.';
302 | } else {
303 | $message = 'Sorry, there was an issue signing you up. Please try again later.';
304 | $this->container->get('logger')->error(sprintf("Error subscribing user to Mailchimp list: %s in %s %s %s", $mailchimpResponse->getContent(), __CLASS__, __FUNCTION__, __LINE__));
305 | }
306 |
307 | $response = new Response();
308 | $response->setStatusCode($mailchimpStatusCode);
309 | $response->setContent($message);
310 | return $response;
311 | }
312 |
313 |
314 | //Don't return full form view for ajax requests
315 | if (!$request->isXmlHttpRequest()) {
316 | return $this->render('forms/subscriber.html.twig', array(
317 | 'subscriberForm' => $subscriberForm->createView(),
318 | ));
319 | } else {
320 |
321 | $response = new Response();
322 | $response->setStatusCode(400);
323 | $response->setContent($this->getFormErrorsAsString($subscriberForm));
324 | return $response;
325 | }
326 | }
327 |
328 | /**
329 | * Returns all form errors for supplied form in a user-friendly string
330 | *
331 | * @param Object $form A Symfony form instance
332 | * @return String $errors A user-legible string of concatenated form errors
333 | */
334 | private function getFormErrorsAsString(&$form)
335 | {
336 | //Parse the form errors and return them as a usable string
337 | $errString = "";
338 | foreach($form as $fieldName => $formField) {
339 | $fieldErrors = $formField->getErrors();
340 | $userFriendlyError = stripslashes(trim($fieldErrors));
341 | $userFriendlyError = str_replace('ERROR:', '', $userFriendlyError);
342 | $userFriendlyError = str_replace(',', '', $userFriendlyError);
343 | $errString .= $userFriendlyError;
344 | }
345 | return $errString;
346 | }
347 |
348 | /**
349 | * Determine if the user is a repeat visitor or not
350 | *
351 | * Returning users will not be challenged with the email submission form
352 | *
353 | * New users will be given the email submission form before they can optimize an article
354 | *
355 | * @param Object - Symfony request
356 | * @return Boolean - true if the user is returning, and false if they are new
357 | */
358 | private function getUserCookieStatus(Request $request)
359 | {
360 | $cookie = $request->cookies->get($this->getParameter('cookie_name'));
361 |
362 | return (isset($cookie) && $cookie === $this->getParameter('cookie_value')) ? true : false;
363 | }
364 |
365 | /**
366 | * Sets the application's cookie and value as specified in parameters.yml
367 | *
368 | * The presence of this cookie, set to the correct value, will mark the user as recognized and returning
369 | * in the future, so that client code allows them to bypass the sign-up flow
370 | *
371 | * The response will be passed by reference, so it will be modified in-place
372 | *
373 | * @param Object - the Symfony response
374 | */
375 | private function setUserCookie(Response &$response)
376 | {
377 | //Set a cookie of specified name with the specified value. Requests for the index page are checked for this cookie
378 | $response->headers->setCookie(new Cookie($this->getParameter('cookie_name'), $this->getParameter('cookie_value')));
379 | }
380 |
381 | }
382 |
--------------------------------------------------------------------------------
/src/AppBundle/DependencyInjection/ConfigValidator.php:
--------------------------------------------------------------------------------
1 | root('advertisements');
14 |
15 | $rootNode
16 | ->arrayNode()
17 | ->children()
18 | ->scalarNode('cta')->isRequired()->end()
19 | ->scalarNode('image_url')->isRequired()->end()
20 | ->scalarNode('url')->isRequired()->end()
21 | ->end()
22 | ->end()
23 | ->end()
24 | ;
25 | return $treeBuilder;
26 | }
27 | }
28 |
29 | ?>
--------------------------------------------------------------------------------
/src/AppBundle/Entity/Article.php:
--------------------------------------------------------------------------------
1 | addPropertyConstraint('body', new NotBlank(
29 | array('message' => 'You must submit the full text of your article.')
30 | )
31 | );
32 |
33 | $metadata->addPropertyConstraint('body', new Length(array(
34 | 'min' => 150,
35 | 'minMessage' => 'Your article must be at least 150 characters long.',
36 | ))
37 | );
38 | }
39 |
40 | /**
41 | * Return the body content of the article
42 | *
43 | * @return String The article content
44 | */
45 | public function getBody()
46 | {
47 | return $this->body;
48 | }
49 |
50 | /**
51 | * Set the body content of the article
52 | *
53 | * @param String $body The article content
54 | */
55 | public function setBody($body)
56 | {
57 | $this->body = $body;
58 | }
59 |
60 | /**
61 | * Return the Datetime the article was created
62 | *
63 | * @return Datetime The article's creation time
64 | */
65 | public function getCreatedDate()
66 | {
67 | return $this->createdDate;
68 | }
69 |
70 | /**
71 | * Sets the Datetime the article was created
72 | *
73 | * @param Datetime $createdDate The article's creation time
74 | */
75 | public function setCreatedDate($createdDate)
76 | {
77 | $this->createdDate = $createdDate;
78 | }
79 |
80 | }
81 |
82 | ?>
--------------------------------------------------------------------------------
/src/AppBundle/Entity/ContactRequest.php:
--------------------------------------------------------------------------------
1 | addPropertyConstraint('body',
25 | new NotBlank(array(
26 | 'message' => 'You must have something more to say!'
27 | )
28 | )
29 | );
30 |
31 | $metadata->addPropertyConstraint('body',
32 | new Length(array(
33 | 'min' => 10,
34 | 'max' => 5000,
35 | 'minMessage' => 'Your message must be at least 10 characters long',
36 | 'maxMessage' => 'Your message cannot be longer than {{ limit }} characters'
37 | )
38 | )
39 | );
40 |
41 | $metadata->addPropertyConstraint('requestor', new Email(array(
42 | 'strict' => false,
43 | 'message' => 'Please enter a valid email address',
44 | 'checkMX' => true,
45 | 'checkHost' => true
46 | )
47 | ));
48 | }
49 |
50 | public function getBody()
51 | {
52 | return $this->body;
53 | }
54 |
55 | public function setBody($body)
56 | {
57 | $this->body = $body;
58 | }
59 |
60 | public function getCreatedDate()
61 | {
62 | return $this->createdDate;
63 | }
64 |
65 | public function setCreatedDate($createdDate)
66 | {
67 | $this->createdDate = $createdDate;
68 | }
69 |
70 | public function getRequestor()
71 | {
72 | return $this->requestor;
73 | }
74 |
75 | public function setRequestor($requestor)
76 | {
77 | $this->requestor = $requestor;
78 | }
79 | }
80 |
81 | ?>
--------------------------------------------------------------------------------
/src/AppBundle/Entity/ReportEmailRecipient.php:
--------------------------------------------------------------------------------
1 | addPropertyConstraint('address', new NotBlank(array(
25 | 'message' => 'You must provide a valid email address')
26 | ));
27 |
28 | $metadata->addPropertyConstraint('address', new Email(array(
29 | 'strict' => false,
30 | 'message' => 'Please provide a valid email address',
31 | 'checkMX' => true,
32 | 'checkHost' => true
33 | )
34 | ));
35 | }
36 |
37 | /**
38 | * Return the recipient email address
39 | *
40 | * @return String The recipient email address
41 | */
42 | public function getAddress()
43 | {
44 | return $this->address;
45 | }
46 |
47 | /**
48 | * Set the recipient email address
49 | *
50 | * @param String $address The recipient email address
51 | */
52 | public function setAddress($address)
53 | {
54 | $this->address = $address;
55 | }
56 |
57 | /**
58 | * Return the final url of the article report
59 | *
60 | * @return String The final url of the article report
61 | */
62 | public function getReportUrl()
63 | {
64 | return $this->reportUrl;
65 | }
66 |
67 | /**
68 | * Set the final url of the article report
69 | *
70 | * @param String The final url of the article report
71 | */
72 | public function setReportUrl($url)
73 | {
74 | $this->reportUrl = $url;
75 | }
76 | }
77 |
78 | ?>
--------------------------------------------------------------------------------
/src/AppBundle/Entity/Subscriber.php:
--------------------------------------------------------------------------------
1 | addPropertyConstraint('address',
22 | new NotBlank(array(
23 | 'message' => 'Please enter your email address.'
24 | )
25 | )
26 | );
27 |
28 | $metadata->addPropertyConstraint('address', new Email(array(
29 | 'strict' => false,
30 | 'message' => 'Please enter a valid email address',
31 | 'checkMX' => true,
32 | 'checkHost' => true
33 | )
34 | ));
35 | }
36 |
37 | public function getAddress()
38 | {
39 | return $this->address;
40 | }
41 |
42 | public function setAddress($address)
43 | {
44 | $this->address = $address;
45 | }
46 | }
47 |
48 | ?>
--------------------------------------------------------------------------------
/src/AppBundle/Twig/AppExtension.php:
--------------------------------------------------------------------------------
1 | array('html'))),
11 | new \Twig_SimpleFunction('legend', array($this ,'generateLegend'), array('is_safe' => array('html'))),
12 | new \Twig_SimpleFunction('advertisement', array($this, 'generateAdBlock'), array('is_safe' => array('html')))
13 | );
14 | }
15 |
16 | /**
17 | * Generates an advertisement block of two ads split into two equal columns
18 | *
19 | * Ads have the following format:
20 | *
21 | * array('
22 | * 'cta' => 'Buy this wonderful product please',
23 | * 'img_path' => 'ads/elegantthemes/300x250.gif',
24 | * 'url' => 'https://www.elegantthemes.com/myaffiliatecode'
25 | * ');
26 | *
27 | * cta is the "Call to Action" - the text that will appear with the ad
28 | *
29 | * img_path is the relative path from web/img/ to the image asset / banner
30 | *
31 | * url is the final url that people who click the ad should be sent to
32 | * this should contain any affiliate tracking information necessary to credit
33 | * your account with conversions that it generates
34 | *
35 | * @param Array $ad1 The associative array representing advertisement 1
36 | * @param Array $ad2 The associative array representing advertisement 2
37 | */
38 | public function generateAdBlock($ad1, $ad2)
39 | {
40 |
41 | return sprintf("", $ad1['cta'], $ad2['cta'], $ad1['url'], $ad1['img_path'], $ad2['url'], $ad2['img_path']);
63 | }
64 |
65 | /**
66 | * Generate a section legend mapping to the bootstrap default classes:
67 | *
68 | * e.g:
69 | * $keys = array('positive', 'neutral', 'negative')
70 | *
71 | * will produce the following legend:
72 | *
73 | * positive = success, neutral = info, negative = danger
74 | *
75 | * @param Array $keys The keys that map to each
76 | * @return String The html of the legend
77 | */
78 | public function generateLegend($title, $keys)
79 | {
80 | if (!isset($title) || !gettype($title) === "string") {
81 | throw new \Exception('generateLegend expects a valid string for a title');
82 | }
83 | if (count($keys) < 3) {
84 | throw new \Exception('generateLegend expects an array containing 3 strings to map to bootstrap classes.');
85 | }
86 | return sprintf("
87 | %s
88 |
89 | %s
90 | %s
91 | %s
92 |
93 | ",$title, $keys[0], $keys[1], $keys[2]);
94 | }
95 |
96 | /**
97 | * Generates a properly-formatted 'bootstrap ribbon' as used on the report page
98 | *
99 | * @param String $title The title that will appear on top of the ribbon
100 | * @return String $html The final html of the ribbon to be rendered on the report
101 | */
102 | public function generateRibbon($title, $type = 'body')
103 | {
104 | //Most ribbons are 'body' types, which ensures they are padded correctly.
105 | //If the second parameter passed to this function is anything but body, it's probably the first ribbon
106 | //which means that we should remove the 'body' class from the ribbon
107 | if ($type != 'body') {
108 | $type = null;
109 | }
110 | return sprintf("
111 | %s
112 |
", $type, $title);
113 | }
114 |
115 | public function getName()
116 | {
117 | return 'app_extension';
118 | }
119 | }
120 |
121 | ?>
--------------------------------------------------------------------------------
/tests/AppBundle/Controller/DefaultControllerTest.php:
--------------------------------------------------------------------------------
1 | request('GET', '/');
14 |
15 | $this->assertEquals(200, $client->getResponse()->getStatusCode());
16 |
17 | }
18 |
19 | public function testIndexFormsPresent()
20 | {
21 | $client = static::createClient();
22 |
23 | $crawler = $client->request('GET', '/');
24 |
25 | //Ensure the article, subscribe and contact forms are present
26 | $this->assertEquals(3, $crawler->filter('form')->count());
27 | }
28 |
29 | public function testIndexTabsPresent()
30 | {
31 | $client = static::createClient();
32 |
33 | $crawler = $client->request('GET', '/');
34 |
35 | //Ensure the optimize, how it works and contact panel nav tabs are present
36 | $this->assertEquals(3, $crawler->filter('.nav-tabs li a')->count());
37 | }
38 |
39 | //Tests that the article form provides an error when
40 | //supplied content is too short
41 | public function testArticleFormSubmitShortContent()
42 | {
43 | $client = static::createClient();
44 |
45 | $crawler = $client->request('GET', '/');
46 |
47 | $articleForm = $this->getArticleForm($crawler);
48 |
49 | $articleForm['form[body]'] = 'This is deliberately too short';
50 |
51 | $crawler = $client->submit($articleForm);
52 |
53 | $this->assertContains('Your article must be at least 150 characters long.', $client->getResponse()->getContent());
54 | }
55 |
56 | //Test that submitting a simple article works and
57 | //results in a redirect response to the written report
58 | public function testArticleFormSubmitArticle()
59 | {
60 | $client = static::createClient();
61 |
62 | $crawler = $client->request('GET', '/');
63 |
64 | $articleForm = $this->getArticleForm($crawler);
65 |
66 | $articleForm['form[body]'] = 'What is Symfony? Symfony is a set of PHP Components, a Web Application framework, a Philosophy, and a Community — all working together in harmony. The leading PHP framework to create websites and web applications. Built on top of the Symfony Components. A set of decoupled and reusable components on which the best PHP applications are built, such as Drupal, phpBB, and eZ Publish. A huge community of Symfony fans committed to take PHP to the next level.';
67 |
68 | $crawler = $client->submit($articleForm);
69 |
70 | $this->assertContains('Redirecting to saved-reports', $client->getResponse()->getContent());
71 | }
72 |
73 | //Test that submitting empty feedback results in an error
74 | public function testContactFormSubmitEmptyContent()
75 | {
76 | $client = static::createClient();
77 |
78 | $crawler = $client->request('GET', '/');
79 |
80 | $contactForm = $this->getContactForm($crawler);
81 |
82 | $contactForm['form[body]'] = '';
83 |
84 | $crawler = $client->submit($contactForm);
85 |
86 | $this->assertContains('You must have something more to say!', $client->getResponse()->getContent());
87 | }
88 |
89 | private function getArticleForm($crawler)
90 | {
91 | return $crawler->filter("form[name='articleForm']")->form();
92 | }
93 |
94 | private function getContactForm($crawler)
95 | {
96 | return $crawler->filter("form[name='contactForm']")->form();
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/var/cache/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/var/cache/.gitkeep
--------------------------------------------------------------------------------
/var/logs/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/var/logs/.gitkeep
--------------------------------------------------------------------------------
/var/sessions/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/var/sessions/.gitkeep
--------------------------------------------------------------------------------
/web/.htaccess:
--------------------------------------------------------------------------------
1 | # Use the front controller as index file. It serves as a fallback solution when
2 | # every other rewrite/redirect fails (e.g. in an aliased environment without
3 | # mod_rewrite). Additionally, this reduces the matching process for the
4 | # start page (path "/") because otherwise Apache will apply the rewriting rules
5 | # to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
6 | DirectoryIndex app.php
7 |
8 | # By default, Apache does not evaluate symbolic links if you did not enable this
9 | # feature in your server configuration. Uncomment the following line if you
10 | # install assets as symlinks or if you experience problems related to symlinks
11 | # when compiling LESS/Sass/CoffeScript assets.
12 | # Options FollowSymlinks
13 |
14 | # Disabling MultiViews prevents unwanted negotiation, e.g. "/app" should not resolve
15 | # to the front controller "/app.php" but be rewritten to "/app.php/app".
16 |
17 | Options -MultiViews
18 |
19 |
20 |
21 | RewriteEngine On
22 |
23 | # Determine the RewriteBase automatically and set it as environment variable.
24 | # If you are using Apache aliases to do mass virtual hosting or installed the
25 | # project in a subdirectory, the base path will be prepended to allow proper
26 | # resolution of the app.php file and to redirect to the correct URI. It will
27 | # work in environments without path prefix as well, providing a safe, one-size
28 | # fits all solution. But as you do not need it in this case, you can comment
29 | # the following 2 lines to eliminate the overhead.
30 | RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$
31 | RewriteRule ^(.*) - [E=BASE:%1]
32 |
33 | # Sets the HTTP_AUTHORIZATION header removed by Apache
34 | RewriteCond %{HTTP:Authorization} .
35 | RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
36 |
37 | # Redirect to URI without front controller to prevent duplicate content
38 | # (with and without `/app.php`). Only do this redirect on the initial
39 | # rewrite by Apache and not on subsequent cycles. Otherwise we would get an
40 | # endless redirect loop (request -> rewrite to front controller ->
41 | # redirect -> request -> ...).
42 | # So in case you get a "too many redirects" error or you always get redirected
43 | # to the start page because your Apache does not expose the REDIRECT_STATUS
44 | # environment variable, you have 2 choices:
45 | # - disable this feature by commenting the following 2 lines or
46 | # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
47 | # following RewriteCond (best solution)
48 | RewriteCond %{ENV:REDIRECT_STATUS} ^$
49 | RewriteRule ^app\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
50 |
51 | # If the requested filename exists, simply serve it.
52 | # We only want to let Apache serve files and not directories.
53 | RewriteCond %{REQUEST_FILENAME} -f
54 | RewriteRule ^ - [L]
55 |
56 | # Rewrite all other queries to the front controller.
57 | RewriteRule ^ %{ENV:BASE}/app.php [L]
58 |
59 |
60 |
61 |
62 | # When mod_rewrite is not available, we instruct a temporary redirect of
63 | # the start page to the front controller explicitly so that the website
64 | # and the generated links can still be used.
65 | RedirectMatch 302 ^/$ /app.php/
66 | # RedirectTemp cannot be used instead
67 |
68 |
69 |
--------------------------------------------------------------------------------
/web/app.php:
--------------------------------------------------------------------------------
1 | loadClassCache();
15 | //$kernel = new AppCache($kernel);
16 |
17 | // When using the HttpCache, you need to call the method in your front controller instead of relying on the configuration parameter
18 | //Request::enableHttpMethodParameterOverride();
19 | $request = Request::createFromGlobals();
20 | $response = $kernel->handle($request);
21 | $response->send();
22 | $kernel->terminate($request, $response);
23 |
--------------------------------------------------------------------------------
/web/app_dev.php:
--------------------------------------------------------------------------------
1 | loadClassCache();
29 | $request = Request::createFromGlobals();
30 | $response = $kernel->handle($request);
31 | $response->send();
32 | $kernel->terminate($request, $response);
33 |
--------------------------------------------------------------------------------
/web/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/apple-touch-icon.png
--------------------------------------------------------------------------------
/web/config.php:
--------------------------------------------------------------------------------
1 | getFailedRequirements();
30 | $minorProblems = $symfonyRequirements->getFailedRecommendations();
31 |
32 | ?>
33 |
34 |
35 |
36 |
37 |
38 | Symfony Configuration Checker
39 |
40 |
41 |
118 |
119 |
120 |
121 |
147 |
148 |
149 |
150 |
151 |
Configuration Checker
152 |
153 | This script analyzes your system to check whether is
154 | ready to run Symfony applications.
155 |
156 |
157 |
158 |
Major problems
159 |
Major problems have been detected and must be fixed before continuing:
160 |
161 |
162 | getHelpHtml() ?>
163 |
164 |
165 |
166 |
167 |
168 |
Recommendations
169 |
170 | Additionally, toTo enhance your Symfony experience,
171 | it’s recommended that you fix the following:
172 |
173 |
174 |
175 | getHelpHtml() ?>
176 |
177 |
178 |
179 |
180 | hasPhpIniConfigIssue()): ?>
181 |
*
182 | getPhpIniConfigPath()): ?>
183 | Changes to the php.ini file must be done in "getPhpIniConfigPath() ?> ".
184 |
185 | To change settings, create a "php.ini ".
186 |
187 |
188 |
189 |
190 |
191 |
All checks passed successfully. Your system is ready to run Symfony applications.
192 |
193 |
194 |
199 |
200 |
201 |
202 |
Symfony Standard Edition
203 |
204 |
205 |
206 |
--------------------------------------------------------------------------------
/web/css/base/reset.css:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html, body, div, span, applet, object, iframe,
7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
8 | a, abbr, acronym, address, big, cite, code,
9 | del, dfn, em, img, ins, kbd, q, s, samp,
10 | small, strike, strong, sub, sup, tt, var,
11 | b, u, i, center,
12 | dl, dt, dd, ol, ul, li,
13 | fieldset, form, label, legend,
14 | table, caption, tbody, tfoot, thead, tr, th, td,
15 | article, aside, canvas, details, embed,
16 | figure, figcaption, footer, header, hgroup,
17 | menu, nav, output, ruby, section, summary,
18 | time, mark, audio, video {
19 | margin: 0;
20 | padding: 0;
21 | border: 0;
22 | font-size: 100%;
23 | font: inherit;
24 | vertical-align: baseline;
25 | }
26 | /* HTML5 display-role reset for older browsers */
27 | article, aside, details, figcaption, figure,
28 | footer, header, hgroup, menu, nav, section {
29 | display: block;
30 | }
31 | body {
32 | line-height: 1;
33 | }
34 | ol, ul {
35 | list-style: none;
36 | }
37 | blockquote, q {
38 | quotes: none;
39 | }
40 | blockquote:before, blockquote:after,
41 | q:before, q:after {
42 | content: '';
43 | content: none;
44 | }
45 | table {
46 | border-collapse: collapse;
47 | border-spacing: 0;
48 | }
--------------------------------------------------------------------------------
/web/css/index/bubbles.css:
--------------------------------------------------------------------------------
1 |
2 | #bubbles {
3 | /*height: 500px;
4 | width: 500px;
5 | position: absolute;
6 | */
7 | margin-left: -130px;
8 | padding: 100px;
9 | position: relative;
10 | left: 50%;
11 | top: 0px;
12 | z-index: 4;
13 | }
14 |
15 |
16 | .bubble {
17 | width: 30px;
18 | height: 30px;
19 | background: #15A93B;
20 | border-radius: 200px;
21 | -moz-border-radius: 200px;
22 | -webkit-border-radius: 200px;
23 | position: absolute;
24 | }
25 |
26 | .x1 {
27 | left: 80px;
28 | -webkit-transform: scale(0.5);
29 | -moz-transform: scale(0.5);
30 | transform: scale(0.5);
31 | opacity: 0.4;
32 | -webkit-animation: moveclouds 4.1s linear infinite, sideWays 4s ease-in-out infinite alternate;
33 | -moz-animation: moveclouds 4.1s linear infinite, sideWays 4s ease-in-out infinite alternate;
34 | -o-animation: moveclouds 4.1s linear infinite, sideWays 4s ease-in-out infinite alternate;
35 | }
36 |
37 | .x2 {
38 | left: 80px;
39 | -webkit-transform: scale(0.5);
40 | -moz-transform: scale(0.5);
41 | transform: scale(0.5);
42 | opacity: 0.5;
43 | -webkit-animation: moveclouds 5s linear infinite, sideWays 5s ease-in-out infinite alternate;
44 | -moz-animation: moveclouds 5s linear infinite, sideWays 5s ease-in-out infinite alternate;
45 | -o-animation: moveclouds 5s linear infinite, sideWays 5s ease-in-out infinite alternate;
46 | }
47 | .x3 {
48 | left: 82px;
49 | -webkit-transform: scale(0.35);
50 | -moz-transform: scale(0.35);
51 | transform: scale(0.35);
52 | opacity: 0.6;
53 | -webkit-animation: moveclouds 3.9s linear infinite, sideWays 4s ease-in-out infinite alternate;
54 | -moz-animation: moveclouds 3.9s linear infinite, sideWays 4s ease-in-out infinite alternate;
55 | -o-animation: moveclouds 3.9s linear infinite, sideWays 4s ease-in-out infinite alternate;
56 | }
57 | .x4 {
58 | left: 84px;
59 | -webkit-transform: scale(0.4);
60 | -moz-transform: scale(0.4);
61 | transform: scale(0.4);
62 | opacity: 0.35;
63 | -webkit-animation: moveclouds 6.1s linear infinite, sideWays 2s ease-in-out infinite alternate;
64 | -moz-animation: moveclouds 6.1s linear infinite, sideWays 2s ease-in-out infinite alternate;
65 | -o-animation: moveclouds 6.1s linear infinite, sideWays 2s ease-in-out infinite alternate;
66 | }
67 | .x5 {
68 | left: 82px;
69 | -webkit-transform: scale(0.2);
70 | -moz-transform: scale(0.2);
71 | transform: scale(0.2);
72 | opacity: 0.8;
73 | -webkit-animation: moveclouds 3.5s linear infinite, sideWays 3s ease-in-out infinite alternate;
74 | -moz-animation: moveclouds 3.5s linear infinite, sideWays 3s ease-in-out infinite alternate;
75 | -o-animation: moveclouds 3.5s linear infinite, sideWays 3s ease-in-out infinite alternate;
76 | }
77 |
78 | .x6 {
79 | left: 86px;
80 | -webkit-transform: scale(0.45);
81 | -moz-transform: scale(0.45);
82 | transform: scale(0.45);
83 | opacity: 0.9;
84 | -webkit-animation: moveclouds 7s linear infinite, sideWays 7s ease-in-out infinite alternate;
85 | -moz-animation: moveclouds 7s linear infinite, sideWays 7s ease-in-out infinite alternate;
86 | -o-animation: moveclouds 7s linear infinite, sideWays 7s ease-in-out infinite alternate;
87 | }
88 |
89 | .x7 {
90 | left: 88px;
91 | -webkit-transform: scale(0.7);
92 | -moz-transform: scale(0.7);
93 | transform: scale(0.7);
94 | opacity: 0.9;
95 | -webkit-animation: moveclouds 4.7s linear infinite, sideWays 6s ease-in-out infinite alternate;
96 | -moz-animation: moveclouds 4.7s linear infinite, sideWays 6s ease-in-out infinite alternate;
97 | -o-animation: moveclouds 4.7s linear infinite, sideWays 6s ease-in-out infinite alternate;
98 | }
99 |
100 | .x8 {
101 | left: 80px;
102 | -webkit-transform: scale(0.6);
103 | -moz-transform: scale(0.6);
104 | transform: scale(0.6);
105 | opacity: 0.4;
106 | -webkit-animation: moveclouds 5.5s linear infinite, sideWays 18s ease-in-out infinite alternate;
107 | -moz-animation: moveclouds 5.5s linear infinite, sideWays 18s ease-in-out infinite alternate;
108 | -o-animation: moveclouds 5.5s linear infinite, sideWays 18s ease-in-out infinite alternate;
109 | }
110 |
111 | .x9 {
112 | left: 82px;
113 | -webkit-transform: scale(0.5);
114 | -moz-transform: scale(0.5);
115 | transform: scale(0.5);
116 | opacity: 0.9;
117 | -webkit-animation: moveclouds 6.2s linear infinite, sideWays 4s ease-in-out infinite alternate;
118 | -moz-animation: moveclouds 6.2s linear infinite, sideWays 4s ease-in-out infinite alternate;
119 | -o-animation: moveclouds 6.2s linear infinite, sideWays 4s ease-in-out infinite alternate;
120 | }
121 |
122 | .x10 {
123 | left: 82px;
124 | -webkit-transform: scale(0.3);
125 | -moz-transform: scale(0.3);
126 | transform: scale(0.3);
127 | opacity: 0.9;
128 | -webkit-animation: moveclouds 7.4s linear infinite, sideWays 2s ease-in-out infinite alternate;
129 | -moz-animation: moveclouds 7.4s linear infinite, sideWays 2s ease-in-out infinite alternate;
130 | -o-animation: moveclouds 7.4s linear infinite, sideWays 2s ease-in-out infinite alternate;
131 | }
132 |
133 | .x11 {
134 | left: 84px;
135 | -webkit-transform: scale(0.4);
136 | -moz-transform: scale(0.4);
137 | transform: scale(0.4);
138 | opacity: 0.9;
139 | -webkit-animation: moveclouds 3.8s linear infinite, sideWays 3s ease-in-out infinite alternate;
140 | -moz-animation: moveclouds 3.8s linear infinite, sideWays 3s ease-in-out infinite alternate;
141 | -o-animation: moveclouds 3.8s linear infinite, sideWays 3s ease-in-out infinite alternate;
142 | }
143 |
144 | .x12 {
145 | left: 86px;
146 | -webkit-transform: scale(0.2);
147 | -moz-transform: scale(0.2);
148 | transform: scale(0.2);
149 | opacity: 0.9;
150 | -webkit-animation: moveclouds 5.7s linear infinite, sideWays 3s ease-in-out infinite alternate;
151 | -moz-animation: moveclouds 5.7s linear infinite, sideWays 3s ease-in-out infinite alternate;
152 | -o-animation: moveclouds 5.7s linear infinite, sideWays 3s ease-in-out infinite alternate;
153 | }
154 |
155 | .x13 {
156 | left: 86px;
157 | -webkit-transform: scale(0.3);
158 | -moz-transform: scale(0.3);
159 | transform: scale(0.3);
160 | opacity: 0.9;
161 | -webkit-animation: moveclouds 4.2s linear infinite, sideWays 2s ease-in-out infinite alternate;
162 | -moz-animation: moveclouds 4.2s linear infinite, sideWays 2s ease-in-out infinite alternate;
163 | -o-animation: moveclouds 4.2s linear infinite, sideWays 2s ease-in-out infinite alternate;
164 | }
165 |
166 | .x14 {
167 | left: 88px;
168 | -webkit-transform: scale(0.1);
169 | -moz-transform: scale(0.1);
170 | transform: scale(0.1);
171 | opacity: 0.7;
172 | -webkit-animation: moveclouds 3.2s linear infinite, sideWays 2s ease-in-out infinite alternate;
173 | -moz-animation: moveclouds 3.2s linear infinite, sideWays 2s ease-in-out infinite alternate;
174 | -o-animation: moveclouds 3.2s linear infinite, sideWays 2s ease-in-out infinite alternate;
175 | }
176 |
177 | .x15 {
178 | left: 88px;
179 | -webkit-transform: scale(0.2);
180 | -moz-transform: scale(0.2);
181 | transform: scale(0.2);
182 | opacity: 0.6;
183 | -webkit-animation: moveclouds 3.4s linear infinite, sideWays 2s ease-in-out infinite alternate;
184 | -moz-animation: moveclouds 3.4s linear infinite, sideWays 2s ease-in-out infinite alternate;
185 | -o-animation: moveclouds 3.4s linear infinite, sideWays 2s ease-in-out infinite alternate;
186 | }
187 |
188 | .x16 {
189 | left: 90px;
190 | -webkit-transform: scale(0.2);
191 | -moz-transform: scale(0.2);
192 | transform: scale(0.2);
193 | opacity: 0.9;
194 | -webkit-animation: moveclouds 4.3s linear infinite, sideWays 3s ease-in-out infinite alternate;
195 | -moz-animation: moveclouds 4.3s linear infinite, sideWays 3s ease-in-out infinite alternate;
196 | -o-animation: moveclouds 4.3s linear infinite, sideWays 3s ease-in-out infinite alternate;
197 | }
198 |
199 | .x17 {
200 | left: 92px;
201 | -webkit-transform: scale(0.4);
202 | -moz-transform: scale(0.4);
203 | transform: scale(0.4);
204 | opacity: 0.6;
205 | -webkit-animation: moveclouds 3.7s linear infinite, sideWays 4s ease-in-out infinite alternate;
206 | -moz-animation: moveclouds 3.7s linear infinite, sideWays 4s ease-in-out infinite alternate;
207 | -o-animation: moveclouds 3.7s linear infinite, sideWays 4s ease-in-out infinite alternate;
208 | }
209 |
210 | .x18 {
211 | left: 92px;
212 | -webkit-transform: scale(0.2);
213 | -moz-transform: scale(0.2);
214 | transform: scale(0.2);
215 | opacity: 0.9;
216 | -webkit-animation: moveclouds 5.2s linear infinite, sideWays 3s ease-in-out infinite alternate;
217 | -moz-animation: moveclouds 5.2s linear infinite, sideWays 3s ease-in-out infinite alternate;
218 | -o-animation: moveclouds 5.2s linear infinite, sideWays 3s ease-in-out infinite alternate;
219 | }
220 |
221 | .x19 {
222 | left: 94px;
223 | -webkit-transform: scale(0.5);
224 | -moz-transform: scale(0.5);
225 | transform: scale(0.5);
226 | opacity: 0.7;
227 | -webkit-animation: moveclouds 4.9s linear infinite, sideWays 5.5s ease-in-out infinite alternate;
228 | -moz-animation: moveclouds 4.9s linear infinite, sideWays 5.5s ease-in-out infinite alternate;
229 | -o-animation: moveclouds 4.9s linear infinite, sideWays 5.5s ease-in-out infinite alternate;
230 | }
231 |
232 | .x20 {
233 | left: 94px;
234 | -webkit-transform: scale(0.3);
235 | -moz-transform: scale(0.3);
236 | transform: scale(0.3);
237 | opacity: 0.9;
238 | -webkit-animation: moveclouds 5.4s linear infinite, sideWays 2s ease-in-out infinite alternate;
239 | -moz-animation: moveclouds 5.4s linear infinite, sideWays 2s ease-in-out infinite alternate;
240 | -o-animation: moveclouds 5.4s linear infinite, sideWays 2s ease-in-out infinite alternate;
241 | }
242 |
243 | .x21 {
244 | left: 96px;
245 | -webkit-transform: scale(0.2);
246 | -moz-transform: scale(0.2);
247 | transform: scale(0.2);
248 | opacity: 0.6;
249 | -webkit-animation: moveclouds 3.9s linear infinite, sideWays 2s ease-in-out infinite alternate;
250 | -moz-animation: moveclouds 3.9s linear infinite, sideWays 2s ease-in-out infinite alternate;
251 | -o-animation: moveclouds 3.9s linear infinite, sideWays 2s ease-in-out infinite alternate;
252 | }
253 |
254 | .x22 {
255 | left: 96px;
256 | -webkit-transform: scale(0.35);
257 | -moz-transform: scale(0.35);
258 | transform: scale(0.35);
259 | opacity: 0.7;
260 | -webkit-animation: moveclouds 6.2s linear infinite, sideWays 3s ease-in-out infinite alternate;
261 | -moz-animation: moveclouds 6.2s linear infinite, sideWays 3s ease-in-out infinite alternate;
262 | -o-animation: moveclouds 6.2s linear infinite, sideWays 3s ease-in-out infinite alternate;
263 | }
264 |
265 | .x22 {
266 | left: 94px;
267 | -webkit-transform: scale(0.4);
268 | -moz-transform: scale(0.4);
269 | transform: scale(0.4);
270 | opacity: 0.6;
271 | -webkit-animation: moveclouds 3.7s linear infinite, sideWays 2.3s ease-in-out infinite alternate;
272 | -moz-animation: moveclouds 3.7s linear infinite, sideWays 2.3s ease-in-out infinite alternate;
273 | -o-animation: moveclouds 3.7s linear infinite, sideWays 2.3s ease-in-out infinite alternate;
274 | }
275 |
276 | .x23 {
277 | left: 90px;
278 | -webkit-transform: scale(0.3);
279 | -moz-transform: scale(0.3);
280 | transform: scale(0.3);
281 | opacity: 0.6;
282 | -webkit-animation: moveclouds 4.4s linear infinite, sideWays 3.5s ease-in-out infinite alternate;
283 | -moz-animation: moveclouds 4.4s linear infinite, sideWays 3.5s ease-in-out infinite alternate;
284 | -o-animation: moveclouds 4.4s linear infinite, sideWays 3.5s ease-in-out infinite alternate;
285 | }
286 |
287 | .x24 {
288 | left: 76px;
289 | -webkit-transform: scale(0.4);
290 | -moz-transform: scale(0.4);
291 | transform: scale(0.4);
292 | opacity: 0.8;
293 | -webkit-animation: moveclouds 3.6s linear infinite, sideWays 4.2s ease-in-out infinite alternate;
294 | -moz-animation: moveclouds 3.6s linear infinite, sideWays 4.2s ease-in-out infinite alternate;
295 | -o-animation: moveclouds 3.6s linear infinite, sideWays 4.2s ease-in-out infinite alternate;
296 | }
297 |
298 | .x25 {
299 | left: 80px;
300 | -webkit-transform: scale(0.4);
301 | -moz-transform: scale(0.4);
302 | transform: scale(0.4);
303 | opacity: 0.6;
304 | -webkit-animation: moveclouds 3.2s linear infinite, sideWays 3s ease-in-out infinite alternate;
305 | -moz-animation: moveclouds 3.2s linear infinite, sideWays 3s ease-in-out infinite alternate;
306 | -o-animation: moveclouds 3.2s linear infinite, sideWays 3s ease-in-out infinite alternate;
307 | }
308 |
309 | .x26 {
310 | left: 82px;
311 | -webkit-transform: scale(0.6);
312 | -moz-transform: scale(0.6);
313 | transform: scale(0.6);
314 | opacity: 0.9;
315 | -webkit-animation: moveclouds 4s linear infinite, sideWays 4s ease-in-out infinite alternate;
316 | -moz-animation: moveclouds 4s linear infinite, sideWays 4s ease-in-out infinite alternate;
317 | -o-animation: moveclouds 4s linear infinite, sideWays 4s ease-in-out infinite alternate;
318 | }
319 |
320 | .x27 {
321 | left: 84px;
322 | -webkit-transform: scale(0.4);
323 | -moz-transform: scale(0.4);
324 | transform: scale(0.4);
325 | opacity: 0.9;
326 | -webkit-animation: moveclouds 4.2s linear infinite, sideWays 2.3s ease-in-out infinite alternate;
327 | -moz-animation: moveclouds 4.2s linear infinite, sideWays 2.3s ease-in-out infinite alternate;
328 | -o-animation: moveclouds 4.2s linear infinite, sideWays 2.3s ease-in-out infinite alternate;
329 | }
330 |
331 | @-webkit-keyframes moveclouds {
332 | 0% {
333 | top: 500px;
334 | }
335 | 100% {
336 | top: -500px;
337 | }
338 | }
339 |
340 | @-webkit-keyframes sideWays {
341 | 0% {
342 | margin-left:0px;
343 | }
344 | 100% {
345 | margin-left:50px;
346 | }
347 | }
348 |
349 | @-moz-keyframes moveclouds {
350 | 0% {
351 | top: 500px;
352 | }
353 |
354 | 100% {
355 | top: -500px;
356 | }
357 | }
358 |
359 | @-moz-keyframes sideWays {
360 | 0% {
361 | margin-left:0px;
362 | }
363 | 100% {
364 | margin-left:50px;
365 | }
366 | }
367 | @-o-keyframes moveclouds {
368 | 0% {
369 | top: 500px;
370 | }
371 | 100% {
372 | top: -500px;
373 | }
374 | }
375 |
376 | @-o-keyframes sideWays {
377 | 0% {
378 | margin-left:0px;
379 | }
380 | 100% {
381 | margin-left:50px;
382 | }
383 | }
--------------------------------------------------------------------------------
/web/css/index/how-it-works.css:
--------------------------------------------------------------------------------
1 | #how-it-works {
2 | background-image: url(../img/backgrounds/subtle_carbon.png);
3 | height: auto;
4 | margin: 10px;
5 | -moz-border-radius: 10px;
6 | -webkit-border-radius: 10px;
7 | border-radius: 10px;
8 |
9 |
10 | }
11 |
12 | .active {
13 | text-shadow: 0px 0px 9px #fff;
14 | }
15 |
16 | #how-it-works li, #how-it-works h3 {
17 | font-family: 'Podkova';
18 | font-weight: bold;
19 | font-size: 23pt;
20 | color: #ED7916;
21 | text-shadow: 0px 2px 3px #994E0E;
22 |
23 | }
24 |
25 | #how-it-works h2 {
26 | padding-left: 86px;
27 | display: inline;
28 | font-family: 'Podkova', serif;
29 | font-size: 36pt;
30 | color: #2CB92C;
31 | text-shadow: 0px 2px 2px #065103;
32 | }
33 |
34 |
35 | #how-it-works p {
36 | margin-left: 5px;
37 | font-family: 'Noto Sans';
38 | font-size: 14pt;
39 |
40 | }
41 |
42 | #how-social {
43 | padding-top: 25px;
44 | padding-left: 220px;
45 | }
46 |
47 | #support {
48 | margin-left: 48px;
49 | }
50 |
51 | #how-it-works ul li i {
52 | text-shadow: none;
53 | }
54 |
55 |
56 |
--------------------------------------------------------------------------------
/web/css/index/index.css:
--------------------------------------------------------------------------------
1 | /* Adjustments for mobile */
2 | @media (max-width: 768px) {
3 | #loading-animation h1 {
4 | font-size: 1em;
5 | top: 40%;
6 | }
7 | #loading-animation .loading-img {
8 | top: 50%;
9 | }
10 | }
11 |
12 | /* Disable Image Dragging and Selecting */
13 | img {
14 | -webkit-user-select: none; /* Chrome/Safari */
15 | -moz-user-select: none; /* Firefox */
16 | -ms-user-select: none; /* IE10+ */
17 | draggable: false;
18 | }
19 |
20 | body {
21 | font-family: 'Helvetica';
22 | padding-top: 100px;
23 | background-color: #000;
24 | }
25 |
26 | p, a, h1, h2, h3, h4, h5, h6, span, li
27 | {
28 | font-family: 'Helvetica';
29 | }
30 |
31 | .tab-content p {
32 | padding: 1em;
33 | color: #fff;
34 | text-shadow: none;
35 | }
36 |
37 | /* Article Textarea */
38 |
39 | textarea {
40 | resize: none;
41 | }
42 |
43 | /* Article & contact form textareas' placeholder text */
44 |
45 | textarea::-webkit-input-placeholder {
46 | font-family: 'Helvetica';
47 | position: relative;
48 | top: 2em;
49 | color: #585550;
50 | font-size: 2em;
51 | text-align: center;
52 | }
53 |
54 | textarea:-moz-placeholder { /* Firefox 18- */
55 | font-family: 'Helvetica';
56 | position: relative;
57 | top: 2em;
58 | color: #585550;
59 | font-size: 2em;
60 | text-align: center;
61 | }
62 |
63 | textarea::-moz-placeholder { /* Firefox 19+ */
64 | font-family: 'Helvetica';
65 | position: relative;
66 | top: 2em;
67 | color: #585550;
68 | font-size: 2em;
69 | text-align: center;
70 | }
71 |
72 | textarea:-ms-input-placeholder {
73 | font-family: 'Helvetica';
74 | position: relative;
75 | top: 2em;
76 | color: #585550;
77 | font-size: 2em;
78 | text-align: center;
79 | }
80 |
81 | textarea {
82 | font-family: 'Helvetica';
83 | min-height: 300px;
84 | width: 100%;
85 | }
86 |
87 | .alert {
88 | margin-top: 1em;
89 | }
90 |
91 | /* Optional contact us response email address placeholder text */
92 | .contact-email::-webkit-input-placeholder {
93 | font-family: 'Helvetica';
94 | color: #585550;
95 | font-size: 1.2em;
96 | text-align: center;
97 | }
98 |
99 | .contact-email:-moz-placeholder {
100 | font-family: 'Helvetica';
101 | color: #585550;
102 | font-size: 1.2em;
103 | text-align: center;
104 | }
105 |
106 | .contact-email::-moz-placeholder { /* Firefox 19+ */
107 | font-family: 'Helvetica';
108 | color: #585550;
109 | font-size: 1.2em;
110 | text-align: center;
111 | }
112 |
113 | .contact-email:-ms-input-placeholder {
114 | font-family: 'Helvetica';
115 | color: #585550;
116 | font-size: 1.2em;
117 | text-align: center;
118 | }
119 |
120 | .submit-button {
121 | font-family: 'Helvetica';
122 | font-size: 2em;
123 | float: right;
124 | width: 100%;
125 | }
126 |
127 | .nav {
128 | margin-top: 2em;
129 | }
130 |
131 | .nav-tabs {
132 | border-bottom: 1px solid #9AD40B;
133 | }
134 |
135 | .nav-tabs li a {
136 | font-size: 1.2em;
137 | color: #0B7A19;
138 | }
139 |
140 | .nav-tabs li.active a {
141 | background-color: #000 !important;
142 | border: 1px solid #9AD40B !important;
143 | font-size: 1.4em;
144 | color: #fff !important;
145 | }
146 |
147 | .nav-tabs li a:hover {
148 | background-color: #000 !important;
149 | border: 1px solid #9AD40B !important;
150 | color: #0B7A19 !important;
151 | }
152 |
153 | .nav-tabs li.active a:hover {
154 | background-color: #000 !important;
155 | border: 1px solid #9AD40B !important;
156 | color: #fff !important;
157 | }
158 |
159 | .signature {
160 | text-align: center;
161 | margin-top: 6em;
162 | padding-bottom: 4em;
163 | }
164 |
165 | .signature a h1 {
166 | color: green;
167 | font-size: 1.5em;
168 | }
169 |
170 | /* Mobile device adjustments */
171 | /* Remove bubbles, shrink logo, adjust placeholder text and submit button text */
172 | @media(max-width: 600px) {
173 | #main-logo {
174 | max-width: 80%;
175 | }
176 | #bubbles {
177 | display: none;
178 | }
179 |
180 | /* Nav */
181 | .nav {
182 | margin-top: 1.2em;
183 | }
184 |
185 | .nav ul {
186 | display: inline-block;
187 | }
188 |
189 | .nav-tabs li {
190 | width: 100%;
191 | float: none;
192 | }
193 |
194 | .nav-tabs li a {
195 | display: inline-block;
196 | font-size: 1em;
197 | color: #0B7A19;
198 | }
199 |
200 | .nav-tabs li.active a {
201 |
202 | font-size: 0.8em;
203 | color: #0B7A19;
204 | }
205 |
206 | .nav-tabs li.active a:hover {
207 | color: #0B7A19;
208 | }
209 |
210 | /* Article & contact form textareas' placeholder text */
211 |
212 | textarea::-webkit-input-placeholder {
213 | position: relative;
214 | top: 1.2em;
215 | color: #585550;
216 | font-size: 1.2em;
217 | text-align: center;
218 | }
219 |
220 | textarea:-moz-placeholder { /* Firefox 18- */
221 | position: relative;
222 | top: 1.2em;
223 | color: #585550;
224 | font-size: 1.2em;
225 | text-align: center;
226 | }
227 |
228 | textarea::-moz-placeholder { /* Firefox 19+ */
229 | position: relative;
230 | top: 1.2em;
231 | color: #585550;
232 | font-size: 1.2em;
233 | text-align: center;
234 | }
235 |
236 | textarea:-ms-input-placeholder {
237 | position: relative;
238 | top: 1.2em;
239 | color: #585550;
240 | font-size: 1.2em;
241 | text-align: center;
242 | }
243 |
244 | textarea {
245 | min-height: 300px;
246 | width: 100%;
247 | }
248 |
249 | /* Optional contact us response email address placeholder text */
250 | .contact-email::-webkit-input-placeholder {
251 | color: #585550;
252 | font-size: 1.2em;
253 | text-align: center;
254 | }
255 |
256 | .contact-email:-moz-placeholder {
257 | color: #585550;
258 | font-size: 1.2em;
259 | text-align: center;
260 | }
261 |
262 | .contact-email::-moz-placeholder { /* Firefox 19+ */
263 | color: #585550;
264 | font-size: 1.2em;
265 | text-align: center;
266 | }
267 |
268 | .contact-email:-ms-input-placeholder {
269 | color: #585550;
270 | font-size: 1.2em;
271 | text-align: center;
272 | }
273 |
274 | .submit-button {
275 | font-size: 1.2em;
276 | float: right;
277 | width: 100%;
278 | }
279 | }
280 |
--------------------------------------------------------------------------------
/web/css/index/loading.css:
--------------------------------------------------------------------------------
1 | .overlay {
2 | /* stretch the overlay by attaching all four corners */
3 | z-index: 3;
4 | font-size: 22pt;
5 | color: #fff;
6 | text-align: center;
7 | position: absolute;
8 | width: 100%;
9 | height: 100%;
10 | min-height: 1600px;
11 | left: 0;
12 | top: 0;
13 | bottom: 0;
14 | right: 0;
15 | /* use any background color you like; rgba would be nice, but older IE will ignore it*/
16 | background: #000;
17 | opacity: 0.8;
18 | filter: alpha(opacity=85);
19 | }
20 |
21 | #overlay h2 {
22 | font-weight: bold;
23 | position: relative;
24 | padding-top: 235px;
25 | padding-bottom: 20px;
26 |
27 | }
28 |
29 | #loading {
30 | width: 200px;
31 | height: 200px;
32 | position: absolute;
33 | z-index: -40000;
34 | /* the image is 50x57, so we use negative margins of 1/2 that size to center it*/
35 | margin: -100px 0 0 -100px;
36 | }
37 |
38 | .loading-text {
39 | position: relative;
40 | top: 55%;
41 | font-size: 2em;
42 | opacity: 1.0;
43 | filter: alpha(opacity=100);
44 | }
45 |
46 | .loading-img {
47 | margin-top: 0.5em;
48 | top: 65%;
49 | left: 50%;
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/web/css/main.css:
--------------------------------------------------------------------------------
1 | @import 'modules/application.scss';
2 | @import 'modules/base.scss';
3 | @import 'modules/buttons.scss';
4 | @import 'modules/contact.scss';
5 | @import 'modules/font-awesome.scss';
6 | @import 'modules/home-nav.scss';
7 | @import 'modules/how-it-works.scss';
8 | @import 'modules/index.scss';
9 | @import 'modules/loading.scss';
10 | @import 'modules/main.scss';
11 | @import 'modules/modals.scss';
12 | @import 'modules/report-ads.scss';
13 | @import 'modules/report-graphs.scss';
14 | @import 'modules/report-table.scss';
15 | @import 'modules/report.scss';
16 | @import 'modules/reset.scss';
17 | @import 'modules/signup-form.scss';
18 |
--------------------------------------------------------------------------------
/web/css/report/report.css:
--------------------------------------------------------------------------------
1 | /* Adjustments for mobile */
2 | @media (max-width: 768px) {
3 | .col-xs-12 label {
4 | padding: 1em 0 1em 0;
5 | }
6 | .ribbon-row button.ribbon h1 {
7 | font-size: 1em;
8 | font-weight: 600;
9 | }
10 | .btn-group.share-buttons a {
11 | margin-top: 0.5em;
12 | font-size: 0.6em;
13 | }
14 | p {
15 | font-size: 1em;
16 | }
17 |
18 | }
19 |
20 | body, p, a, h1, h2, h3, h4, h5, h6, span, li, label
21 | {
22 | font-family: 'Helvetica';
23 | }
24 |
25 | input[type="submit"]:active {
26 | padding: 0;
27 | }
28 |
29 | button::-moz-focus-inner {
30 | border: 0;
31 | }
32 |
33 | .hidden {
34 | display: none;
35 | }
36 |
37 | /* Bootstrap Ribbon BEGIN */
38 | .jumbotron {
39 | background-image: url('../img/backgrounds/groovepaper.png');
40 | margin-top: -10px;
41 | }
42 |
43 | .jumbotron p {
44 | font-size: 18px;
45 | }
46 |
47 | .jumbotron.first, .report-backing {
48 | margin-bottom: 0;
49 | }
50 |
51 | .link-holder {
52 | padding-bottom: 2em;
53 | }
54 |
55 | span.form-control.article-pedestal {
56 | height: 19em;
57 | overflow: scroll;
58 | }
59 |
60 | button {
61 | min-height: 60px;
62 | margin: 20px 0;
63 | line-height: 34px;
64 | position: relative;
65 | cursor: pointer;
66 | user-select: none;
67 | outline: none !important;
68 | width: 100%;
69 | }
70 |
71 | .report-send-button {
72 | min-height: 0;
73 | min-width: 0;
74 | }
75 |
76 | button:active {
77 | outline: none;
78 | }
79 |
80 | button.ribbon {
81 | outline: none;
82 | outline-color: transparent;
83 | }
84 |
85 | button.ribbon:hover {
86 | background-color: #337ab7;
87 | }
88 |
89 | button.ribbon h1, button.ribbon-wrapped {
90 | font-size: 2.5em;
91 | }
92 |
93 | button.ribbon.body {
94 | margin-top: -10px;
95 | }
96 |
97 | button.ribbon:before,
98 | button.ribbon:after {
99 | top: 8px;
100 | z-index: -10;
101 | }
102 |
103 | button.ribbon:before {
104 | border-color: #53dab6 #53dab6 #53dab6 transparent;
105 | left: -35px;
106 | border-width: 20px;
107 | }
108 |
109 | button.ribbon:after {
110 | border-color: #53dab6 transparent #53dab6 #53dab6;
111 | right: -35px;
112 | border-width: 20px;
113 | }
114 |
115 | button:before,
116 | button:after {
117 | content: '';
118 | position: absolute;
119 | height: 0;
120 | width: 0;
121 | border-style: solid;
122 | border-width: 0;
123 | outline: none;
124 | }
125 |
126 | button.btn-default:before {
127 | border-color: #757575 #757575 #757575 transparent;
128 | }
129 |
130 | button.btn-default:after {
131 | border-color: #757575 transparent #757575 #757575;
132 | }
133 |
134 | button.btn-primary:before {
135 | border-color: #2e6da4 #2e6da4 #2e6da4 transparent;
136 | }
137 |
138 | button.btn-primary:after {
139 | border-color: #2e6da4 transparent #2e6da4 #2e6da4;
140 | }
141 |
142 | button.btn-success:before {
143 | border-color: #398439 #398439 #398439 transparent;
144 | }
145 |
146 | button.btn-success:after {
147 | border-color: #398439 transparent #398439 #398439;
148 | }
149 |
150 | button.btn-info:before {
151 | border-color: #269abc #269abc #269abc transparent;
152 | }
153 |
154 | button.btn-info:after {
155 | border-color: #269abc transparent #269abc #269abc;
156 | }
157 |
158 | button.btn-warning:before {
159 | border-color: #d58512 #d58512 #d58512 transparent;
160 | }
161 |
162 | button.btn-warning:after {
163 | border-color: #d58512 transparent #d58512 #d58512;
164 | }
165 |
166 | button.btn-danger:before {
167 | border-color: #ac2925 #ac2925 #ac2925 transparent;
168 | }
169 |
170 | button.btn-danger:after {
171 | border-color: #ac2925 transparent #ac2925 #ac2925;
172 | }
173 | /* END Bootstrap Ribbon */
174 |
175 |
176 | /* Disable UI Elements touch activity / highlighting behavior */
177 |
178 | .noselect {
179 | -webkit-touch-callout: none;
180 | /* iOS Safari */
181 | -webkit-user-select: none;
182 | /* Chrome/Safari/Opera */
183 | -khtml-user-select: none;
184 | /* Konqueror */
185 | -moz-user-select: none;
186 | /* Firefox */
187 | -ms-user-select: none;
188 | /* Internet Explorer/Edge */
189 | user-select: none;
190 | /* Non-prefixed version, currently not supported by any browser */
191 | }
192 |
193 | /* Bootstrap Ribbon END */
194 |
195 |
196 | /*-----BODY------*/
197 |
198 | body {
199 | -webkit-touch-callout: none;
200 | -webkit-user-select: none;
201 | -khtml-user-select: none;
202 | -moz-user-select: none;
203 | -ms-user-select: none;
204 | user-select: none;
205 | }
206 |
207 | body {
208 | background-image: url(../img/backgrounds/navy_blue.png);
209 | min-height: 1000px;
210 | height: 100%;
211 | }
212 |
213 | body.report {
214 | background-image: url(../img/backgrounds/paper.png);
215 | }
216 |
217 | img.opening-report-ribbon {
218 | position: absolute;
219 | left: 0;
220 | }
221 |
222 | /* BEGIN Advertisement blocks */
223 | .ad {
224 | margin: 0 auto;
225 | }
226 | /* END Advertisement blocks */
227 |
228 | .row.images img.img-responsive {
229 | max-width: 200px;
230 | max-height: 200px;
231 | }
232 |
233 | #error h1 {
234 | text-align: center;
235 | color: #D62901;
236 | font-size: 92pt;
237 | }
238 |
239 | #container {
240 | position: absolute;
241 | left: 50%;
242 | /*top: 50%;*/
243 | width: 800px;
244 | height: 650px;
245 | /*margin-top: -325px; */
246 | margin-left: -400px;
247 | /*background-image: url(brushed_alu.png);*/
248 | }
249 |
250 | #text-box {
251 | position: absolute;
252 | left: 50%;
253 | top: 50%;
254 | width: 750px;
255 | height: 600px;
256 | margin-top: -300px;
257 | margin-left: -375px;
258 | }
259 |
260 | #results-container {
261 | position: absolute;
262 | left: 50%;
263 | top: 50%;
264 | width: 1200px;
265 | height: 1050px;
266 | margin-top: -725px;
267 | margin-left: -800px;
268 | background-color: white;
269 | }
270 |
271 | #report-container {
272 | margin: 0 auto;
273 | max-width: 1260px;
274 | overflow: hidden;
275 | padding-bottom: 20px;
276 | }
277 |
278 | textarea {
279 | resize: none;
280 | /*z-index: 3;*/
281 | border: none;
282 | }
283 |
284 | #selector input {
285 | font-size: 36pt;
286 | }
287 |
288 | textarea::-webkit-input-placeholder {
289 | font-family: 'Helvetica';
290 | }
291 |
292 | textarea:-moz-placeholder { /* Firefox 18- */
293 | font-family: 'Helvetica';
294 | }
295 |
296 | textarea::-moz-placeholder { /* Firefox 19+ */
297 | font-family: 'Helvetica';
298 | }
299 |
300 | textarea:-ms-input-placeholder {
301 | font-family: 'Helvetica';
302 | }
303 |
304 | #summary {
305 | padding: 12px;
306 | }
307 |
308 | #bufferDiv {
309 | margin-bottom: 765px;
310 | }
311 |
312 | .report-intro-text {
313 | margin: 1em 0 0 0.5em;
314 | padding: 0.5em;
315 | }
316 |
317 | /*-----------GRAPH STYLES-------------*/
318 |
319 |
320 | /*-------------IMAGES TABLE--------------*/
321 |
322 | .loading-div .fa-spinner {
323 | margin-top: 1em;
324 | }
325 |
326 | #images {
327 | position: relative;
328 | overflow: scroll;
329 | top: -100px;
330 | width: 100%;
331 | }
332 |
333 | .image-container {
334 | position: relative;
335 | max-width: 200px;
336 | max-height: 200px;
337 | }
338 |
339 | .image-container .overlay {
340 | position: absolute;
341 | top: 0;
342 | left: 0;
343 | width: 100%;
344 | height: 100%;
345 | display: none;
346 | color: #fff;
347 | }
348 |
349 | .image-container:hover .overlay {
350 | display: block;
351 | background: rgba(0, 0, 0, .6);
352 | }
353 |
354 |
355 | /* End pulled from main.css */
356 |
357 | #results-container {
358 | position: absolute;
359 | left: 50%;
360 | top: 50%;
361 | width: 1200px;
362 | height: 1050px;
363 | margin-top: -725px;
364 | margin-left: -800px;
365 | background-color: white;
366 | }
367 |
368 | #report-container {
369 | background-image: url(../img/backgrounds/debut_light.png);
370 | margin: 0 auto;
371 | max-width: 1260px;
372 | overflow: hidden;
373 | padding-bottom: 20px;
374 | }
375 |
376 | body.report {
377 | }
378 |
379 | #summary {
380 | padding: 12px;
381 | }
382 |
383 | #bufferDiv {
384 | margin-bottom: 765px;
385 | }
386 |
387 | #images {
388 | position: relative;
389 | overflow: scroll;
390 | top: -100px;
391 | width: 100%;
392 | }
393 |
394 | .overlay {
395 | display: none;
396 | opacity: 1;
397 | background-color: rgba(50,50,50,1);
398 | position: absolute;
399 | height: 100%;
400 | width: 100%;
401 | overflow: hidden;
402 | top: 0;
403 | left: 0;
404 | display: none;
405 | }
406 |
407 | .overlay.text-center {
408 | margin-top: 1em;
409 | }
410 |
411 | ul.img-row{
412 | list-style:none;
413 | margin:0;
414 | padding:0;
415 | }
416 | .ratio-4-3{
417 | width:100%;
418 | position:relative;
419 | background:url() 50% 50% no-repeat;
420 | background-size:cover;
421 | background-clip:content-box;
422 | }
423 | .ratio-4-3:before{
424 | display:block;
425 | content:"";
426 | padding-top:75%;
427 | }
--------------------------------------------------------------------------------
/web/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/favicon.ico
--------------------------------------------------------------------------------
/web/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/web/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/web/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/web/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/web/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/web/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/web/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/web/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/web/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/web/img/ads/MarketSamuraiBanner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/ads/MarketSamuraiBanner.jpg
--------------------------------------------------------------------------------
/web/img/ads/bluehost/300x250/bh_300x250_01.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/ads/bluehost/300x250/bh_300x250_01.gif
--------------------------------------------------------------------------------
/web/img/ads/bluehost/300x250/bh_300x250_02.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/ads/bluehost/300x250/bh_300x250_02.gif
--------------------------------------------------------------------------------
/web/img/ads/bluehost/300x250/bh_300x250_03.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/ads/bluehost/300x250/bh_300x250_03.gif
--------------------------------------------------------------------------------
/web/img/ads/bluehost/300x250/bh_300x250_04.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/ads/bluehost/300x250/bh_300x250_04.jpg
--------------------------------------------------------------------------------
/web/img/ads/bluehost/300x250/bh_300x250_05.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/ads/bluehost/300x250/bh_300x250_05.gif
--------------------------------------------------------------------------------
/web/img/ads/elegantthemes/300x250.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/ads/elegantthemes/300x250.gif
--------------------------------------------------------------------------------
/web/img/ads/semrush_banner_300_250.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/ads/semrush_banner_300_250.png
--------------------------------------------------------------------------------
/web/img/article-optimizer-example-report.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/article-optimizer-example-report.png
--------------------------------------------------------------------------------
/web/img/article-optimizer-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/article-optimizer-logo.png
--------------------------------------------------------------------------------
/web/img/backgrounds/groovepaper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/backgrounds/groovepaper.png
--------------------------------------------------------------------------------
/web/img/backgrounds/navy_blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/backgrounds/navy_blue.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/blue-oval-100-percent-optimized.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/blue-oval-100-percent-optimized.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/brown-100-percent-optimized.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/brown-100-percent-optimized.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/brown-leather-100-percent-optimized.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/brown-leather-100-percent-optimized.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/excellent-article-award.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/excellent-article-award.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/excellent-content-award.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/excellent-content-award.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/excellent-content.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/excellent-content.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/fantastic-article-award.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/fantastic-article-award.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/fantastic-content-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/fantastic-content-badge.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/green-100-percent-optimized.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/green-100-percent-optimized.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/optimal-article.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/optimal-article.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/optimized-content-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/optimized-content-badge.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/optimized.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/optimized.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/perfect-article-award.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/perfect-article-award.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/perfect-article.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/perfect-article.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/seo-optimized-article.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/seo-optimized-article.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/silver-circle-100-percent-optimized.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/silver-circle-100-percent-optimized.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/super-article-award.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/super-article-award.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/super-content.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/super-content.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/superb-content-award.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/superb-content-award.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/well-done-article-award.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/well-done-article-award.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/winning-article-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/winning-article-badge.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/winning-content-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/winning-content-badge.png
--------------------------------------------------------------------------------
/web/img/badges/100-percent-optimized/winning-writing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/100-percent-optimized/winning-writing.png
--------------------------------------------------------------------------------
/web/img/badges/categories/arts-entertainment/art-entertainment-award.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/categories/arts-entertainment/art-entertainment-award.png
--------------------------------------------------------------------------------
/web/img/badges/categories/business/business-article-award.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/categories/business/business-article-award.png
--------------------------------------------------------------------------------
/web/img/badges/categories/computer-internet/computers-internet-award.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/categories/computer-internet/computers-internet-award.png
--------------------------------------------------------------------------------
/web/img/badges/categories/culture-politics/culture-politics-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/categories/culture-politics/culture-politics-badge.png
--------------------------------------------------------------------------------
/web/img/badges/categories/gaming/gaming-award.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/categories/gaming/gaming-award.png
--------------------------------------------------------------------------------
/web/img/badges/categories/health/health-content-award.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/categories/health/health-content-award.png
--------------------------------------------------------------------------------
/web/img/badges/categories/law-crime/law-and-crime-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/categories/law-crime/law-and-crime-badge.png
--------------------------------------------------------------------------------
/web/img/badges/categories/recreation/recreation-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/categories/recreation/recreation-badge.png
--------------------------------------------------------------------------------
/web/img/badges/categories/religion/religion-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/categories/religion/religion-badge.png
--------------------------------------------------------------------------------
/web/img/badges/categories/science-technology/science.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/categories/science-technology/science.png
--------------------------------------------------------------------------------
/web/img/badges/categories/sports/sports-award.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/categories/sports/sports-award.png
--------------------------------------------------------------------------------
/web/img/badges/categories/unknown/default-article-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/categories/unknown/default-article-badge.png
--------------------------------------------------------------------------------
/web/img/badges/categories/weather/weather-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/badges/categories/weather/weather-badge.png
--------------------------------------------------------------------------------
/web/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/favicon.png
--------------------------------------------------------------------------------
/web/img/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zackproser/articleoptimizer/d6b7b82c24f178524e1f621bd6f4c4a189602370/web/img/loading.gif
--------------------------------------------------------------------------------
/web/js/clipboard.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * clipboard.js v1.5.12
3 | * https://zenorocha.github.io/clipboard.js
4 | *
5 | * Licensed MIT © Zeno Rocha
6 | */
7 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.Clipboard=t()}}(function(){var t,e,n;return function t(e,n,o){function i(a,c){if(!n[a]){if(!e[a]){var s="function"==typeof require&&require;if(!c&&s)return s(a,!0);if(r)return r(a,!0);var l=new Error("Cannot find module '"+a+"'");throw l.code="MODULE_NOT_FOUND",l}var u=n[a]={exports:{}};e[a][0].call(u.exports,function(t){var n=e[a][1][t];return i(n?n:t)},u,u.exports,t,e,n,o)}return n[a].exports}for(var r="function"==typeof require&&require,a=0;ao;o++)n[o].fn.apply(n[o].ctx,e);return this},off:function(t,e){var n=this.e||(this.e={}),o=n[t],i=[];if(o&&e)for(var r=0,a=o.length;a>r;r++)o[r].fn!==e&&o[r].fn._!==e&&i.push(o[r]);return i.length?n[t]=i:delete n[t],this}},e.exports=o},{}],8:[function(e,n,o){!function(i,r){if("function"==typeof t&&t.amd)t(["module","select"],r);else if("undefined"!=typeof o)r(n,e("select"));else{var a={exports:{}};r(a,i.select),i.clipboardAction=a.exports}}(this,function(t,e){"use strict";function n(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var i=n(e),r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol?"symbol":typeof t},a=function(){function t(t,e){for(var n=0;n Copied!');
20 | setTimeout(function() {
21 | $(button).html(' Click to Copy');
22 | }, 900);
23 | });
24 |
25 | //On copy bitly url success, display visual confirmation
26 | clipboard.on('success', function(e) {
27 | $('#copy-link-button').tooltip('show');
28 | setTimeout(function() {
29 | $('#copy-link-button').tooltip('hide');
30 | }, 1200);
31 | });
32 |
33 | //On image hover, display the copy to clipboard overlay
34 | $(ctrl.imageThumbnail).on('mouseenter', function() {
35 | $(this).find('.overlay').fadeIn(400);
36 | }).on('mouseleave', function() {
37 | $(this).find('.overlay').stop().fadeOut(100);
38 | });
39 |
40 | //Bind email share button to show report emailing modal
41 | $(ctrl.emailReportButton).on('click', function() {
42 | $(ctrl.emailReportModal).modal('show');
43 | });
44 |
45 | //Set Facebook share button to current report url
46 | $(ctrl.facebookShareButton).attr('href', $(ctrl.facebookShareButton).attr('href') + window.location.href);
47 |
48 | //Process requests to email the report to someone
49 | $(ctrl.sendReportButton).on('click', function(e) {
50 | e.preventDefault();
51 |
52 | function showSuccessAlert(msg) {
53 | $(ctrl.sendReportSuccessAlert).html(msg).removeClass('hidden').show();
54 | resetSendReportModal();
55 | }
56 |
57 | function showErrorAlert(msg) {
58 | $(ctrl.sendReportErrorAlert).html(msg).removeClass('hidden').show();
59 | resetSendReportModal();
60 | }
61 |
62 | function resetSendReportModal() {
63 | setTimeout(function() {
64 | $(ctrl.sendReportSuccessAlert).html('').hide();
65 | $(ctrl.sendReportErrorAlert).html('').hide();
66 | }, 1200);
67 | }
68 |
69 | var values = {};
70 | $.each($(ctrl.sendReportForm).serializeArray(), function(i, field) {
71 | values[field.name] = field.value;
72 | });
73 |
74 | values.uri = window.location.href;
75 |
76 | var requestOptions = {
77 | url: '/email-report',
78 | method: 'POST',
79 | data: values,
80 | success: function(data) {
81 | showSuccessAlert(data);
82 | setTimeout(function() {
83 | ctrl.emailReportModal.modal('hide');
84 | }, 1200);
85 | },
86 | error: function(error) {
87 | showErrorAlert(error);
88 | }
89 | }
90 | $.ajax(requestOptions);
91 | });
92 | }
93 |
94 | function showShortlinkPending() {
95 | $(ctrl.copyLinkButton).html(' Linking..');
96 | }
97 |
98 | function updateBitlyUrl(url) {
99 | $(ctrl.copyLinkButton).html('Copy Link');
100 | $('#report-link-holder').val(url);
101 | }
102 |
103 | function renderBitlyError(error) {
104 | $(ctrl.copyLinkButton).html('Copy Link');
105 | $(ctrl.bitlyErrorAlert).html('Sorry, there was an issue obtaining a shortlink.')
106 | .removeClass('hidden')
107 | .show();
108 | }
109 |
110 | function getBitlyLink() {
111 |
112 | showShortlinkPending();
113 |
114 | var url = window.location.href;
115 | var requestOptions = {
116 | method: 'POST',
117 | url: '/bitly-shorten',
118 | data: {
119 | url: url
120 | },
121 | success: function(data) {
122 | if (data && typeof data === "string") {
123 | var response = JSON.parse(data);
124 | }
125 | if (response.status_code && response.status_code === 200 && response.data.url) {
126 | updateBitlyUrl(response.data.url);
127 | addBitlyLinkToTweetPrefill(response.data.url);
128 | } else {
129 | renderBitlyError();
130 | }
131 | },
132 | error: function(error) {
133 | this.renderBitlyError();
134 | }
135 | };
136 | $.ajax(requestOptions);
137 | };
138 |
139 | //Add the bitly link to the pre-formatted tweet
140 | function addBitlyLinkToTweetPrefill(url) {
141 | if (typeof url === "string" && url != null) {
142 | var
143 | originalHref = $(ctrl.tweetButton).attr('href'),
144 | newHref = originalHref += ' ' + url;
145 | $(ctrl.tweetButton).attr('href', newHref);
146 | }
147 | }
148 |
149 | getBitlyLink();
150 |
151 | getFlickrImages();
152 |
153 | return {
154 | init: init
155 | }
156 |
157 | })();
--------------------------------------------------------------------------------
/web/js/report.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function() {
2 |
3 | var controls = {};
4 |
5 | controls.copyLinkButton = $('#copy-link-button');
6 | controls.emailReportButton = $('#email-report-button');
7 | controls.emailReportModal = $('#email-report-modal');
8 | controls.sendReportButton = $('#form_sendReport');
9 | controls.sendReportForm = $('form');
10 | controls.sendReportSuccessAlert = $(controls.emailReportModal).find('.alert.alert-success');
11 | controls.sendReportErrorAlert = $(controls.emailReportModal).find('.alert.alert-danger');
12 | controls.facebookShareButton = $('#facebook-share-button');
13 | controls.imageThumbnail = $('.img-thumbnail');
14 | controls.bitlyErrorAlert = $('#bitly-error');
15 | controls.tweetButton = $('#tweet-button');
16 |
17 | ReportClient.init(controls);
18 |
19 | });
20 |
21 |
--------------------------------------------------------------------------------
/web/robots.txt:
--------------------------------------------------------------------------------
1 | # www.robotstxt.org/
2 | # www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449
3 |
4 | User-agent: *
5 | Disallow:
6 |
--------------------------------------------------------------------------------