├── config ├── readme.txt ├── package.nuspec ├── package.xml └── package.manifest ├── app ├── views │ ├── serp1.png │ ├── serp2.png │ └── epiphany.seo.metadata.html ├── Epiphany.SeoMetadata │ ├── .nuget │ │ ├── NuGet.exe │ │ ├── NuGet.Config │ │ └── NuGet.targets │ ├── SeoMetadata.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── app.config │ ├── SeoMetadataStartup.cs │ ├── Epiphany.SeoMetadata.sln │ ├── SeoMetadataUrlSegmentProvider.cs │ ├── packages.config │ ├── SeoMetadataPropertyValueConverter.cs │ └── Epiphany.SeoMetadata.csproj ├── scripts │ ├── filters │ │ └── ellipsisLimit.filter.js │ ├── directives │ │ └── maxlen.directive.js │ └── controllers │ │ └── metadata.controller.js └── styles │ └── epiphany.seo.metadata.scss ├── images ├── example1.gif ├── epiphany-logo.png ├── too-much-data.png └── property-editor-options.png ├── test ├── app.conf.js ├── specs │ └── EpiphanySeoMetadataController.spec.js └── karma.conf.js ├── publish.cmd ├── appveyor.yml ├── LICENSE ├── package.json ├── .gitignore ├── README.md └── Gruntfile.js /config/readme.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/serp1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlewis/seo-metadata/HEAD/app/views/serp1.png -------------------------------------------------------------------------------- /app/views/serp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlewis/seo-metadata/HEAD/app/views/serp2.png -------------------------------------------------------------------------------- /images/example1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlewis/seo-metadata/HEAD/images/example1.gif -------------------------------------------------------------------------------- /images/epiphany-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlewis/seo-metadata/HEAD/images/epiphany-logo.png -------------------------------------------------------------------------------- /images/too-much-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlewis/seo-metadata/HEAD/images/too-much-data.png -------------------------------------------------------------------------------- /images/property-editor-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlewis/seo-metadata/HEAD/images/property-editor-options.png -------------------------------------------------------------------------------- /app/Epiphany.SeoMetadata/.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlewis/seo-metadata/HEAD/app/Epiphany.SeoMetadata/.nuget/NuGet.exe -------------------------------------------------------------------------------- /app/Epiphany.SeoMetadata/.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/app.conf.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('umbraco', [ 2 | 'umbraco.filters', 3 | 'umbraco.directives', 4 | 'umbraco.resources', 5 | 'umbraco.services', 6 | 'umbraco.mocks', 7 | 'umbraco.security', 8 | 'ngCookies' 9 | ]); -------------------------------------------------------------------------------- /app/Epiphany.SeoMetadata/SeoMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Epiphany.SeoMetadata 2 | { 3 | public class SeoMetadata 4 | { 5 | public string Title { get; set; } 6 | public string Description { get; set; } 7 | public bool NoIndex { get; set; } 8 | public string UrlName { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /publish.cmd: -------------------------------------------------------------------------------- 1 | REM Get the most recently built package and upload it 2 | FOR /F %%I IN ('DIR pkg\nuget\*.nupkg /b /O:-D') DO set PKG=%%I & GOTO :pushpkg 3 | 4 | :pushpkg 5 | echo Pushing package %PKG% 6 | app\Epiphany.SeoMetadata\.nuget\nuget.exe push pkg\nuget\%PKG% -Source https://www.myget.org/F/epiphany/api/v2/package 7 | app\Epiphany.SeoMetadata\.nuget\nuget.exe push pkg\nuget\%PKG% -------------------------------------------------------------------------------- /app/scripts/filters/ellipsisLimit.filter.js: -------------------------------------------------------------------------------- 1 | angular.module('umbraco').filter('ellipsisLimit', function () { 2 | return function (value, max) { 3 | if (!value) { 4 | return ''; 5 | } 6 | 7 | max = parseInt(max, 10); 8 | if (!max) { 9 | return value; 10 | } 11 | 12 | if (value.length <= max) { 13 | return value; 14 | } 15 | 16 | value = value.substr(0, max); 17 | return value + ' …'; 18 | }; 19 | }); -------------------------------------------------------------------------------- /app/Epiphany.SeoMetadata/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("Epiphany.SeoMetadata")] 5 | [assembly: AssemblyDescription("")] 6 | [assembly: AssemblyConfiguration("")] 7 | [assembly: AssemblyCompany("Epiphany Solutions")] 8 | [assembly: AssemblyProduct("Epiphany.SeoMetadata")] 9 | [assembly: AssemblyCopyright("Copyright © Epiphany Solutions 2015")] 10 | [assembly: AssemblyTrademark("")] 11 | [assembly: AssemblyCulture("")] 12 | [assembly: ComVisible(false)] 13 | 14 | [assembly: Guid("422fa9e6-ef4d-4125-8344-4f66cd447ad0")] 15 | 16 | [assembly: AssemblyVersion("0.3.0")] 17 | [assembly: AssemblyFileVersion("0.3.0")] -------------------------------------------------------------------------------- /app/Epiphany.SeoMetadata/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 0.4.0.{build} 2 | skip_non_tags: false 3 | clone_folder: c:\seo-metadata 4 | build_script: 5 | - cmd: >- 6 | cd c:\seo-metadata 7 | 8 | 9 | nuget restore app\Epiphany.SeoMetadata\Epiphany.SeoMetadata.sln 10 | 11 | 12 | npm install 13 | 14 | npm install grunt@0.4.5 --save-dev 15 | 16 | npm install -g grunt-cli 17 | 18 | grunt nuget 19 | 20 | grunt package 21 | artifacts: 22 | - path: pkg\nuget\* 23 | - path: pkg\umbraco\* 24 | nuget: 25 | account_feed: true 26 | project_feed: true 27 | deploy: 28 | - provider: NuGet 29 | api_key: 30 | secure: knxgwH5a9amElcEhLWera7bfkrOIO5YFQvDuJ9T7nN93bsbarPSWfaRM05nsu5iE 31 | artifact: /.*\.nupkg/ 32 | on: 33 | branch: master 34 | -------------------------------------------------------------------------------- /config/package.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= name %> 5 | <%= version %> 6 | <%= author %> 7 | <%= author %> 8 | <%= description %> 9 | false 10 | http://opensource.org/licenses/MIT 11 | https://github.com/ryanlewis/seo-metadata 12 | 13 | umbraco seo metadata propertyeditor ryanlewis epiphany 14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/scripts/directives/maxlen.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('umbraco').directive('maxlen', function () { 2 | return { 3 | require: 'ngModel', 4 | link: function (scope, el, attrs, ctrl) { 5 | 6 | var validate = false; 7 | var length = 999999; 8 | 9 | if (attrs.name === 'title') { 10 | validate = scope.model.config.allowLongTitles !== '1'; 11 | length = scope.serpTitleLength; 12 | } else if (attrs.name === 'description') { 13 | validate = scope.model.config.allowLongDescriptions !== '1'; 14 | length = scope.serpDescriptionLength; 15 | } 16 | 17 | ctrl.$parsers.unshift(function (viewValue) { 18 | if (validate && viewValue.length > length) { 19 | ctrl.$setValidity('maxlen', false); 20 | } else { 21 | ctrl.$setValidity('maxlen', true); 22 | } 23 | 24 | return viewValue; 25 | }); 26 | } 27 | }; 28 | }); 29 | -------------------------------------------------------------------------------- /app/Epiphany.SeoMetadata/SeoMetadataStartup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using Umbraco.Core; 4 | using Umbraco.Core.Logging; 5 | using Umbraco.Core.Strings; 6 | 7 | namespace Epiphany.SeoMetadata 8 | { 9 | public class SeoMetadataStartup : ApplicationEventHandler 10 | { 11 | protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) 12 | { 13 | bool ignoreSegmentProvider; 14 | 15 | var hasKey = Boolean.TryParse(ConfigurationManager.AppSettings["SeoMetadata.NoSegmentProvider"], out ignoreSegmentProvider); 16 | 17 | if (!hasKey || !ignoreSegmentProvider) 18 | { 19 | UrlSegmentProviderResolver.Current.InsertTypeBefore(typeof(DefaultUrlSegmentProvider), typeof(SeoMetadataUrlSegmentProvider)); 20 | LogHelper.Info("Configured SeoMetadataUrlSegmentProvider"); 21 | } 22 | 23 | base.ApplicationStarting(umbracoApplication, applicationContext); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ryan Lewis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /config/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= name %> 6 | <%= version %> 7 | <%= license %> 8 | <%= url %> 9 | 10 | 0 11 | 0 12 | 0 13 | 14 | 15 | 16 | <%= author %> 17 | <%= authorUrl %> 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | <% files.forEach(function(file) { %> 32 | 33 | <%= file.guid %>.<%= file.ext %> 34 | <%= file.dir %> 35 | <%= file.name %> 36 | 37 | <% }); %> 38 | 39 | 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Epiphany.SeoMetadata", 3 | "version": "0.4.0", 4 | "description": "An Umbraco property editor for SEO metadata", 5 | "license": "MIT", 6 | "licenseUrl": "http://opensource.org/licenses/MIT", 7 | "author": { 8 | "name": "Ryan Lewis", 9 | "email": "ryan@wpyz.org", 10 | "url": "http://ryanl.me" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/ryanlewis/seo-metadata" 15 | }, 16 | "devDependencies": { 17 | "grunt": "~0.4.5", 18 | "grunt-bump": "^0.7.0", 19 | "grunt-contrib-clean": "~1.0.0", 20 | "grunt-contrib-concat": "~1.0.0", 21 | "grunt-contrib-copy": "~1.0.0", 22 | "grunt-contrib-jshint": "^1.0.0", 23 | "grunt-contrib-watch": "~1.0.0", 24 | "grunt-dotnet-assembly-info": "^1.0.19", 25 | "grunt-karma": "^0.12.2", 26 | "grunt-mkdir": "~1.0.0", 27 | "grunt-msbuild": "^0.3.6", 28 | "grunt-nuget": "~0.1.5", 29 | "grunt-sass": "^1.1.0", 30 | "grunt-template": "~0.2.3", 31 | "grunt-umbraco-package": "~1.0.0", 32 | "karma": "^0.13.22", 33 | "karma-jasmine": "^0.3.8", 34 | "karma-phantomjs-launcher": "^1.0.0", 35 | "load-grunt-tasks": "~3.4.1", 36 | "node-sass": "^3.4.2", 37 | "time-grunt": "~1.3.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Epiphany.SeoMetadata/Epiphany.SeoMetadata.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Epiphany.SeoMetadata", "Epiphany.SeoMetadata.csproj", "{56046752-1E1B-420B-B1D1-DB47BE67FCD0}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{CB67878B-C1C9-4AEC-8F82-BCBFF8153AB4}" 9 | ProjectSection(SolutionItems) = preProject 10 | .nuget\NuGet.Config = .nuget\NuGet.Config 11 | .nuget\NuGet.exe = .nuget\NuGet.exe 12 | .nuget\NuGet.targets = .nuget\NuGet.targets 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {56046752-1E1B-420B-B1D1-DB47BE67FCD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {56046752-1E1B-420B-B1D1-DB47BE67FCD0}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {56046752-1E1B-420B-B1D1-DB47BE67FCD0}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {56046752-1E1B-420B-B1D1-DB47BE67FCD0}.Release|Any CPU.Build.0 = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(SolutionProperties) = preSolution 27 | HideSolutionNode = FALSE 28 | EndGlobalSection 29 | EndGlobal 30 | -------------------------------------------------------------------------------- /app/Epiphany.SeoMetadata/SeoMetadataUrlSegmentProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using System.Globalization; 4 | using Newtonsoft.Json; 5 | using Umbraco.Core; 6 | using Umbraco.Core.Models; 7 | using Umbraco.Core.Strings; 8 | 9 | namespace Epiphany.SeoMetadata 10 | { 11 | public class SeoMetadataUrlSegmentProvider : IUrlSegmentProvider 12 | { 13 | private static readonly string PropertyName = "metadata"; 14 | 15 | static SeoMetadataUrlSegmentProvider() 16 | { 17 | var propertyName = ConfigurationManager.AppSettings["SeoMetadata.PropertyName"]; 18 | if (!String.IsNullOrWhiteSpace(propertyName)) 19 | { 20 | PropertyName = propertyName; 21 | } 22 | } 23 | 24 | public string GetUrlSegment(IContentBase content) 25 | { 26 | if (!content.HasProperty(PropertyName)) return null; 27 | 28 | try 29 | { 30 | var metadata = JsonConvert.DeserializeObject(content.GetValue(PropertyName)); 31 | if (metadata == null || String.IsNullOrWhiteSpace(metadata.UrlName)) return null; 32 | return metadata.UrlName.ToUrlSegment(); 33 | } 34 | catch 35 | { 36 | return null; 37 | } 38 | } 39 | 40 | public string GetUrlSegment(IContentBase content, CultureInfo culture) 41 | { 42 | return GetUrlSegment(content); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /test/specs/EpiphanySeoMetadataController.spec.js: -------------------------------------------------------------------------------- 1 | describe('Epiphany.SeoMetadata tests', function() { 2 | var $scope, $location, $rootScope, createController; 3 | 4 | beforeEach(module('umbraco')); 5 | 6 | beforeEach(inject(function ($rootScope, $controller, angularHelper, entityMocks, mocksUtils) { 7 | 8 | //mock the scope model 9 | $scope = $rootScope.$new(); 10 | $scope.model = { 11 | alias: "property", 12 | label: "Epiphany.SeoMetadata property", 13 | description: "desc", 14 | config: {} 15 | }; 16 | 17 | //setup the controller for the test by setting its scope to 18 | //our mocked model 19 | createController = function() { 20 | return $controller('EpiphanySeoMetadataController', { 21 | '$scope': $scope 22 | }); 23 | }; 24 | })); 25 | 26 | it('model.value should be equal to meh', function() { 27 | var controller = createController(); 28 | 29 | //initially our model.value is not set 30 | expect($scope.model.value).toBe(undefined); 31 | 32 | //we then set it to meh 33 | $scope.model.value = "meh"; 34 | 35 | //and the test should pass 36 | expect($scope.model.value).toBe("meh"); 37 | }); 38 | 39 | it('config should be set', function() { 40 | var controller = createController(); 41 | 42 | //Our config should not be null 43 | expect($scope.model.config).not.toBeNull(); 44 | 45 | }); 46 | }); -------------------------------------------------------------------------------- /app/Epiphany.SeoMetadata/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/Epiphany.SeoMetadata/SeoMetadataPropertyValueConverter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Configuration; 4 | using Umbraco.Core.Logging; 5 | using Umbraco.Core.Models.PublishedContent; 6 | using Umbraco.Core.PropertyEditors; 7 | 8 | namespace Epiphany.SeoMetadata 9 | { 10 | [PropertyValueType(typeof (SeoMetadata))] 11 | [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] 12 | public class SeoMetadataPropertyValueConverter : PropertyValueConverterBase 13 | { 14 | private const string RecurseAppSettingKey = "SeoMetadata.RecurseIfDefaults"; 15 | private static readonly bool RecurseIfDefaults; 16 | 17 | static SeoMetadataPropertyValueConverter() 18 | { 19 | RecurseIfDefaults = false; 20 | if (ConfigurationManager.AppSettings[RecurseAppSettingKey] != null) 21 | { 22 | Boolean.TryParse(ConfigurationManager.AppSettings[RecurseAppSettingKey], out RecurseIfDefaults); 23 | } 24 | } 25 | 26 | public override bool IsConverter(PublishedPropertyType propertyType) 27 | { 28 | return propertyType.PropertyEditorAlias != null && propertyType.PropertyEditorAlias.Equals("Epiphany.SeoMetadata"); 29 | } 30 | 31 | public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) 32 | { 33 | if (source == null) return null; 34 | var sourceString = source.ToString(); 35 | if (String.IsNullOrWhiteSpace(sourceString)) return null; 36 | 37 | try 38 | { 39 | var md = JsonConvert.DeserializeObject(sourceString); 40 | 41 | if (RecurseIfDefaults && 42 | String.IsNullOrWhiteSpace(md.Title) && 43 | String.IsNullOrWhiteSpace(md.Description) && 44 | String.IsNullOrWhiteSpace(md.UrlName)) 45 | { 46 | return null; 47 | } 48 | 49 | return md; 50 | } 51 | catch (Exception e) 52 | { 53 | LogHelper.Warn(String.Format("Cannot deserialize SeoMetadata - {0} - {1}", 54 | e.GetType().Name, e.Message)); 55 | return null; 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Tue Jun 03 2014 08:15:43 GMT+0200 (CEST) 3 | 4 | module.exports = function(config) { 5 | 6 | config.set({ 7 | 8 | // base path that will be used to resolve all patterns (eg. files, exclude) 9 | basePath: '..', 10 | 11 | 12 | // frameworks to use 13 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 14 | frameworks: ['jasmine'], 15 | 16 | 17 | // list of files / patterns to load in the browser 18 | files: [ 19 | 'test/assets/lib/jquery/jquery-2.0.3.min.js', 20 | 'test/assets/lib/angular/1.1.5/angular.js', 21 | 'test/assets/lib/angular/1.1.5/angular-cookies.min.js', 22 | 'test/assets/lib/angular/1.1.5/angular-mocks.js', 23 | 'test/assets/lib/angular/angular-ui-sortable.js', 24 | 25 | 'test/assets/lib/underscore/underscore.js', 26 | 'test/assets/lib/umbraco/Extensions.js', 27 | 'test/assets/lib/lazyload/lazyload.min.js', 28 | 29 | 'test/app.conf.js', 30 | 'test/assets/js/umbraco.*.js', 31 | 32 | 'app/scripts/controllers/*.js', 33 | 'test/**/*.spec.js' 34 | ], 35 | 36 | 37 | // list of files to exclude 38 | exclude: [ 39 | 'test/assets/js/umbraco.httpbackend.js' 40 | ], 41 | 42 | 43 | // preprocess matching files before serving them to the browser 44 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 45 | preprocessors: { 46 | 47 | }, 48 | 49 | 50 | // test results reporter to use 51 | // possible values: 'dots', 'progress' 52 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 53 | reporters: ['progress'], 54 | 55 | 56 | // web server port 57 | port: 9876, 58 | 59 | 60 | // enable / disable colors in the output (reporters and logs) 61 | colors: true, 62 | 63 | 64 | // level of logging 65 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 66 | logLevel: config.LOG_INFO, 67 | 68 | 69 | // enable / disable watching file and executing tests whenever any file changes 70 | autoWatch: false, 71 | 72 | 73 | // start these browsers 74 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 75 | browsers: ['PhantomJS'], 76 | 77 | 78 | // Continuous Integration mode 79 | // if true, Karma captures browsers, runs the tests and exits 80 | singleRun: true 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /config/package.manifest: -------------------------------------------------------------------------------- 1 | { 2 | "propertyEditors": [ 3 | { 4 | "name": "SEO Metadata", 5 | "alias": "Epiphany.SeoMetadata", 6 | "editor": { 7 | "valueType": "JSON", 8 | "view": "~/App_Plugins/Epiphany.SeoMetadata/views/epiphany.seo.metadata.html" 9 | }, 10 | "prevalues": { 11 | "fields": [ 12 | { 13 | "label": "Allow long titles", 14 | "description": "If ticked, long titles will not invalidate the form", 15 | "key": "allowLongTitles", 16 | "view": "boolean" 17 | }, 18 | { 19 | "label": "Allow long descriptions", 20 | "description": "If ticked, long descriptions will not invalidate the form", 21 | "key": "allowLongDescriptions", 22 | "view": "boolean" 23 | }, 24 | { 25 | "label": "SERP Title Length", 26 | "description": "The length that Google starts stripping/hiding characters. 65 is roughly where Google will start truncating the title.", 27 | "key": "serpTitleLength", 28 | "view": "number" 29 | }, 30 | { 31 | "label": "SERP Description Length", 32 | "description": "The length that Google starts stripping/hiding characters. 150 characters is roughly where Google will start truncating the description.", 33 | "key": "serpDescriptionLength", 34 | "view": "number" 35 | }, 36 | { 37 | "label": "Developer Name", 38 | "description": "The name of the agency (for example, \"Epiphany\") to show on the control. This is used in the help descriptions.", 39 | "key": "developerName", 40 | "view": "textstring" 41 | }, 42 | { 43 | "label": "Site Title", 44 | "description": "The default title that should be appended to the title e.g.  | My Company", 45 | "key": "siteTitle", 46 | "view": "textstring" 47 | }, 48 | { 49 | "label": "Do Not Index explanation", 50 | "description": "An explanation what the effect of checking 'Do Not Index' is. This is used in the help description.", 51 | "key": "doNotIndexExplanation", 52 | "view": "textstring" 53 | } 54 | ] 55 | } 56 | } 57 | ], 58 | "css": [ 59 | "~/App_Plugins/Epiphany.SeoMetadata/css/epiphany.seo.metadata.css" 60 | ], 61 | "javascript": [ 62 | "~/App_Plugins/Epiphany.SeoMetadata/js/epiphany.seo.metadata.js" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /app/styles/epiphany.seo.metadata.scss: -------------------------------------------------------------------------------- 1 | $bp: 1500px; 2 | 3 | .seo-metadata-container { 4 | 5 | input[name=title] { 6 | width: 90%; 7 | } 8 | 9 | label { 10 | padding-left: 0; 11 | } 12 | 13 | .title-length { 14 | font-size: 0.75em; 15 | color: #ccc; 16 | top: -5px; 17 | position: relative; 18 | } 19 | 20 | .help-block { 21 | margin: 10px 0 35px 0; 22 | font-size: 12px; 23 | line-height: 16px; 24 | opacity: 0.5; 25 | max-width: 90%; 26 | p { 27 | margin-bottom: 14px; 28 | } 29 | } 30 | 31 | .error { 32 | color: rgb(185, 74, 72); 33 | } 34 | 35 | textarea { 36 | width: 90%; 37 | height: 97px; 38 | } 39 | 40 | .controls-col { 41 | @media (max-width: $bp) { 42 | width: 100%; 43 | } 44 | } 45 | 46 | .serp-col { 47 | padding-left: 30px; 48 | position: relative; 49 | @media (max-width: $bp) { 50 | margin-left: 210px !important; 51 | clear: left; 52 | width: 70%; 53 | } 54 | 55 | .serp-preview-label { 56 | font-size: 1em; 57 | font-weight: bold; 58 | display: none; 59 | border-bottom: 1px #ccc solid; 60 | a { 61 | cursor: pointer; 62 | } 63 | 64 | @media (max-width: $bp) { 65 | display: block; 66 | } 67 | } 68 | } 69 | 70 | .serp-preview { 71 | 72 | min-height: 120px; 73 | max-width: 520px; 74 | //border: 1px #ccc solid; 75 | border-radius: 2px; 76 | 77 | //padding: 40px 20px; 78 | padding: 10px 0; 79 | color: #1a0dab; 80 | font-family: arial, sans-serif; 81 | zoom: 1; 82 | 83 | h3 { 84 | font-size: 18px; 85 | line-height: 21.600000381469727px; 86 | margin: 0; 87 | padding: 0; 88 | } 89 | 90 | a { 91 | &:hover, &:visited { 92 | text-decoration: none; 93 | } 94 | 95 | color: rgb(0, 102, 33); 96 | line-height: 16px; 97 | font-size: 13px; 98 | } 99 | 100 | p { 101 | margin: 0; 102 | max-width: 504px; 103 | color: rgb(84, 84, 84); 104 | font-size: 13px; 105 | line-height: 18.200000762939453px; 106 | } 107 | 108 | .listing { 109 | margin: 23px 0; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | tmp 4 | pkg 5 | 6 | ## Ignore Visual Studio temporary files, build results, and 7 | ## files generated by popular Visual Studio add-ons. 8 | 9 | # User-specific files 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | build/ 26 | bld/ 27 | [Bb]in/ 28 | [Oo]bj/ 29 | 30 | # Visual Studo 2015 cache/options directory 31 | .vs/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | *_i.c 47 | *_p.c 48 | *_i.h 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.tmp_proj 63 | *.log 64 | *.vspscc 65 | *.vssscc 66 | .builds 67 | *.pidb 68 | *.svclog 69 | *.scc 70 | 71 | # Chutzpah Test files 72 | _Chutzpah* 73 | 74 | # Visual C++ cache files 75 | ipch/ 76 | *.aps 77 | *.ncb 78 | *.opensdf 79 | *.sdf 80 | *.cachefile 81 | 82 | # Visual Studio profiler 83 | *.psess 84 | *.vsp 85 | *.vspx 86 | 87 | # TFS 2012 Local Workspace 88 | $tf/ 89 | 90 | # Guidance Automation Toolkit 91 | *.gpState 92 | 93 | # ReSharper is a .NET coding add-in 94 | _ReSharper*/ 95 | *.[Rr]e[Ss]harper 96 | *.DotSettings.user 97 | 98 | # JustCode is a .NET coding addin-in 99 | .JustCode 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | _NCrunch_* 109 | .*crunch*.local.xml 110 | 111 | # MightyMoose 112 | *.mm.* 113 | AutoTest.Net/ 114 | 115 | # Web workbench (sass) 116 | .sass-cache/ 117 | 118 | # Installshield output folder 119 | [Ee]xpress/ 120 | 121 | # DocProject is a documentation generator add-in 122 | DocProject/buildhelp/ 123 | DocProject/Help/*.HxT 124 | DocProject/Help/*.HxC 125 | DocProject/Help/*.hhc 126 | DocProject/Help/*.hhk 127 | DocProject/Help/*.hhp 128 | DocProject/Help/Html2 129 | DocProject/Help/html 130 | 131 | # Click-Once directory 132 | publish/ 133 | 134 | # Publish Web Output 135 | *.[Pp]ublish.xml 136 | *.azurePubxml 137 | # TODO: Comment the next line if you want to checkin your web deploy settings 138 | # but database connection strings (with potential passwords) will be unencrypted 139 | *.pubxml 140 | *.publishproj 141 | 142 | # NuGet Packages 143 | *.nupkg 144 | # The packages folder can be ignored because of Package Restore 145 | **/packages/* 146 | # except build/, which is used as an MSBuild target. 147 | !**/packages/build/ 148 | # Uncomment if necessary however generally it will be regenerated when needed 149 | #!**/packages/repositories.config 150 | 151 | # Windows Azure Build Output 152 | csx/ 153 | *.build.csdef 154 | 155 | # Windows Store app package directory 156 | AppPackages/ 157 | 158 | # Others 159 | *.[Cc]ache 160 | ClientBin/ 161 | [Ss]tyle[Cc]op.* 162 | ~$* 163 | *~ 164 | *.dbmdl 165 | *.dbproj.schemaview 166 | *.pfx 167 | *.publishsettings 168 | node_modules/ 169 | bower_components/ 170 | 171 | # RIA/Silverlight projects 172 | Generated_Code/ 173 | 174 | # Backup & report files from converting an old project file 175 | # to a newer Visual Studio version. Backup files are not needed, 176 | # because we have git ;-) 177 | _UpgradeReport_Files/ 178 | Backup*/ 179 | UpgradeLog*.XML 180 | UpgradeLog*.htm 181 | 182 | # SQL Server files 183 | *.mdf 184 | *.ldf 185 | 186 | # Business Intelligence projects 187 | *.rdl.data 188 | *.bim.layout 189 | *.bim_*.settings 190 | 191 | # Microsoft Fakes 192 | FakesAssemblies/ 193 | 194 | # Node.js Tools for Visual Studio 195 | .ntvs_analysis.dat 196 | 197 | # Visual Studio 6 build log 198 | *.plg 199 | 200 | # Visual Studio 6 workspace options file 201 | *.opt 202 | 203 | # Note: VisualStudio gitignore rules may also be relevant 204 | 205 | # Ignore unimportant folders generated by Umbraco 206 | **/App_Data/ClientDependency/ 207 | **/App_Data/ExamineIndexes/ 208 | **/App_Data/Logs/ 209 | **/App_Data/[Pp]review/ 210 | **/App_Data/TEMP/ 211 | Cached/ 212 | 213 | # Ignore Umbraco content cache file 214 | **/App_Data/umbraco.config 215 | 216 | # Don't ignore Umbraco packages (VisualStudio.gitignore mistakes this for a NuGet packages folder) 217 | # Make sure to include details from VisualStudio.gitignore BEFORE this 218 | !**/App_Data/[Pp]ackages/ 219 | !**/[Uu]mbraco/[Dd]eveloper/[Pp]ackages 220 | test/seo-metadata-test/ 221 | -------------------------------------------------------------------------------- /app/views/epiphany.seo.metadata.html: -------------------------------------------------------------------------------- 1 | 104 | -------------------------------------------------------------------------------- /app/scripts/controllers/metadata.controller.js: -------------------------------------------------------------------------------- 1 | angular.module("umbraco") 2 | .controller("EpiphanySeoMetadataController", 3 | [ 4 | '$scope', 'contentResource', function($scope, contentResource) { 5 | $scope.invalidate = true; 6 | $scope.model.hideLabel = true; 7 | $scope.serpTitleLength = !$scope.model.config.serpTitleLength ? $scope.model.config.serpTitleLength : 65; 8 | $scope.serpDescriptionLength = !$scope.model.config.serpDescriptionLength ? $scope.model.config.serpDescriptionLength : 150; 9 | $scope.developerName = $scope.model.config.developerName || 'your agency'; 10 | $scope.doNotIndexExplanation = $scope.model.config.doNotIndexExplanation || ''; 11 | $scope.siteTitle = $scope.model.config.siteTitle || ''; 12 | 13 | // default model.value 14 | if (!$scope.model.value) { 15 | $scope.model.value = { title: '', description: '', urlName: '', noIndex: false }; 16 | } 17 | 18 | // a very basic slugify function to replace chars in url 19 | function slugify(text) { 20 | return text.toString() 21 | .toLowerCase() 22 | .trim() 23 | .replace(/\s+/g, '-') // Replace spaces with - 24 | .replace(/&/g, '') // Replace & with nothing 25 | .replace(/[^\w\-]+/g, '') // Remove all non-word chars 26 | .replace(/\-\-+/g, '-'); // Replace multiple - with single - 27 | } 28 | 29 | $scope.init = function() { 30 | var content = $scope.GetParentContent(); 31 | if (!content.published) { 32 | // get the URL of the parent document for later 33 | contentResource.getById(content.parentId).then(function(data) { 34 | $scope.parentUrl = data.urls[0]; 35 | }); 36 | } 37 | }; 38 | 39 | $scope.GetUrl = function() { 40 | 41 | var name, url; 42 | var pageContent = $scope.GetParentContent(); 43 | 44 | // handle instances where the document is newly created and unpublished 45 | if (!pageContent.published) { 46 | if ($scope.model.value.urlName && $scope.model.value.urlName.length) { 47 | name = slugify($scope.model.value.urlName); 48 | } else if (typeof(pageContent.name) !== 'undefined') { 49 | name = slugify(pageContent.name); 50 | } 51 | 52 | if (typeof(name) === 'undefined' || name.length === 0) { 53 | name = "unpublished-page"; 54 | } 55 | 56 | return $scope.ProtocolAndHost() + $scope.parentUrl + name + "/"; 57 | } 58 | 59 | var nodeUrl = pageContent.urls[0]; 60 | 61 | // test if it is an absolute url: http://stackoverflow.com/a/10687158 62 | url = /^https?:\/\//i.test(nodeUrl) ? nodeUrl : $scope.ProtocolAndHost() + nodeUrl; 63 | 64 | var urlSplit = url.split('/'); 65 | 66 | // we got a slug configured? 67 | if ($scope.model.value.urlName && $scope.model.value.urlName.length) { 68 | 69 | // http://mydomain.com will be split into an array of 3 strings: 'http:', '' and 'mydomain.com' 70 | // only handle slugify of urlName under root level 71 | if (urlSplit.length > 4) { 72 | if (urlSplit[urlSplit.length - 1] === "") { 73 | urlSplit[urlSplit.length - 2] = slugify($scope.model.value.urlName); 74 | } else { 75 | urlSplit[urlSplit.length - 1] = slugify($scope.model.value.urlName); 76 | } 77 | } 78 | 79 | // join new values 80 | url = urlSplit.join('/'); 81 | } else { 82 | // use the name of the document instead 83 | urlSplit.splice(-2, 2); // pop the last slug off the end of the array, use the document name instead 84 | name = pageContent.name ? slugify(pageContent.name) : 'unpublished-page'; 85 | url = urlSplit.join('/') + '/' + name + '/'; 86 | } 87 | 88 | return url; 89 | }; 90 | 91 | $scope.ProtocolAndHost = function() { 92 | 93 | var http = location.protocol; 94 | var slashes = http.concat("//"); 95 | return slashes.concat(window.location.hostname); 96 | }; 97 | 98 | $scope.GetTitle = function () { 99 | 100 | var title = $scope.model.value.title || 'Page Title'; 101 | 102 | if (title !== 'Page Title' && $scope.siteTitle !== '') { 103 | title += $scope.siteTitle.replace(' ', ' '); 104 | } 105 | return title; 106 | }; 107 | 108 | $scope.GetParentContent = function() { 109 | var currentScope = $scope.$parent; 110 | 111 | for (var i = 0; i < 150; i++) { 112 | if (currentScope.content) { 113 | return currentScope.content; 114 | } 115 | 116 | currentScope = currentScope.$parent; 117 | } 118 | 119 | return null; 120 | }; 121 | 122 | 123 | $(window).resize(function() { 124 | $scope.$apply(function() { 125 | if (window.innerWidth <= 1500 && !$scope.hideSerp) { 126 | $scope.hideSerp = true; 127 | } 128 | if (window.innerWidth > 1500) { 129 | $scope.hideSerp = false; 130 | } 131 | }); 132 | }); 133 | 134 | $scope.init(); 135 | } 136 | ]); 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SEO Metadata for Umbraco 2 | 3 | **This package is no longer maintained. See https://github.com/ryanlewis/seo-metadata/issues/35** 4 | 5 | [![Build status](https://ci.appveyor.com/api/projects/status/bodaqgqs54rtjys0?svg=true)](https://ci.appveyor.com/project/ryanlewis/seo-metadata) 6 | 7 | SEO Metadata for Umbraco is a property editor that is used for maintaining common SEO-related information for a page. It gives users a visual representation of how the page would look on a Google search result page and hints to when the title and description is too long, with optional validation. 8 | 9 | ![SEO metadata](https://raw.githubusercontent.com/ryanlewis/seo-metadata/master/images/example1.gif) 10 | 11 | ## Recent Changes 12 | 13 | **0.4.0** 14 | 15 | * Better handling when multiple domains are configured (#20, #23 - thanks @bjarnef!) 16 | * Ability to configure an appended title to SEO titles (#22 - thanks @JJCLane!) 17 | 18 | **0.3.0** 19 | 20 | * Allow for recursive property values if all the fields are blank and the AppSettingKey is set. 21 | 22 | **0.2.1** 23 | 24 | * Fixed some issues with Umbraco package 25 | * Resolved issue with PropertyEditorValueConverter returning null if page is saved when control is unused 26 | 27 | **0.2.0** 28 | 29 | * Fixed issue with custom URL Names not working if `SeoMetadata.NoSegmentProvider` appSetting wasn't present 30 | * Added new option to set the developer name 31 | 32 | **0.1.0** 33 | 34 | * Initial release 35 | 36 | 37 | ## Installation 38 | 39 | Install the latest version through NuGet. 40 | ``` 41 | Install-Package Epiphany.SeoMetadata 42 | ``` 43 | 44 | After installing via Nuget, create a property editor of type **SEO Metadata** and include on your page. We recommend the property name **"metadata"** to work with all features out-of-the-box (see the [URL Name](#using-the-url-name) section for configuration options) 45 | 46 | ![Property editor options](https://raw.githubusercontent.com/ryanlewis/seo-metadata/master/images/property-editor-options.png) 47 | 48 | Alternatively, if you want to hack around with the project, you can fork, checkout and develop locally. See the [Developing SEO Metadata](#developing-seo-metadata) section. 49 | 50 | ## Configuration 51 | 52 |
53 |
Allow long titles
54 |
If ticked, long titles will not invalidate the property editor.
55 | 56 |
Allow long descriptions
57 |
If ticked, long descriptions will not invalidate the property editor.
58 | 59 |
SERP Title Length
60 |
The maximum length of a title. This isn't an exact number, so your mileage may vary. The default value of 65 is a conservative value that should work for most cases. Google will truncate overly long titles with ellipses (…)
61 | 62 |
SERP Description Length
63 |
The maximum length of the description. This isn't an exact number, so your mileage may vary. The default value of 150 is a conservative value that should work for most cases. Google will truncate overly long descriptions with ellipses (…)
64 | 65 |
Developer Name
66 |
Allows you to personalise the template a bit by putting the name of your company/agency/other. This is used within the descriptions on the view and is displayed to your content editors.
67 | 68 |
Site Title
69 |
Allows you to configure a site title that is appended to the title (e.g. " | My Company")
70 |
71 | 72 | ## Usage 73 | 74 | The SEO Metadata is stored as JSON, so can be used dynamically. 75 | 76 | ```c# 77 | Title: @CurrentPage.Metadata.Title 78 | Description: @CurrentPage.Metadata.Description 79 | Do Not Index?: @CurrentPage.Metadata.NoIndex 80 | URL Name: @CurrentPage.Metadata.UrlName 81 | ``` 82 | 83 | A [Property Editor Value Converter][1] is installed for getting a strongly-typed **SeoMetadata** instance. 84 | 85 | ```c# 86 | @{ 87 | var metadata = Model.Content.GetPropertyValue("metadata"); 88 | } 89 | 90 | Title: @metadata.Title 91 | Description: @metadata.Description 92 | Do Not Index?: @metadata.NoIndex 93 | URL Name: @metadata.UrlName 94 | ``` 95 | 96 | The following snippet can be used for using the **Do Not Index** checkbox. 97 | 98 | ```c# 99 | @if (Model.Content.GetPropertyValue("metadata").NoIndex) 100 | { 101 | 102 | } 103 | ``` 104 | 105 | If you're a fan of [ZpqrtBnk Umbraco Models Builder][2], you can add something like the following in your partial class 106 | 107 | ```c# 108 | [ImplementPropertyType("metadata")] 109 | public virtual SeoMetadata Metadata 110 | { 111 | get { return this.GetPropertyValue("metadata"); } 112 | } 113 | ``` 114 | 115 | ### Using the URL Name 116 | 117 | SEO Metadata also installs a [UrlSegmentProvider][3] to ensure the URL Name property works as intended. By default, it expects your SEO Metadata property to be called `metadata`. You can configure this property by adding the following setting to your appSettings in your web.config 118 | 119 | ```xml 120 | 121 | ``` 122 | 123 | If you want to disable the `SeoMetadataUrlSegmentProvider` altogether (to add manually, or implement yourself), you can set the following appSetting to disable it. 124 | ```xml 125 | 126 | ``` 127 | 128 | ### Recursive Values 129 | 130 | You will find that `recursive: true` will not work as an instance of the `SeoMetadata` class will always return. You can override this behaviour so that 131 | the resolver returns null. To do this, add the following appSetting key. 132 | ```xml 133 | 134 | ``` 135 | 136 | [1]:https://our.umbraco.org/documentation/extending-umbraco/Property-Editors/PropertyEditorValueConverters 137 | [2]:https://github.com/zpqrtbnk/Zbu.ModelsBuilder 138 | [3]:https://our.umbraco.org/documentation/Reference/Request-Pipeline/outbound-pipeline#segments 139 | 140 | ## Developing SEO Metadata 141 | 142 | ### Checkout the project 143 | ```bash 144 | git clone https://github.com/ryanlewis/seo-metadata.git 145 | cd seo-metadata 146 | ``` 147 | 148 | ### Install Dependencies 149 | 150 | ```bash 151 | npm install -g grunt-cli 152 | npm install 153 | ``` 154 | 155 | ### Build 156 | 157 | ```bash 158 | build.cmd 159 | grunt 160 | ``` 161 | 162 | If you wish to build it to a local Umbraco directory, use the `target` option. 163 | 164 | ```bash 165 | grunt --target=c:\dev\path-to-umbraco-root-dir 166 | ``` 167 | -------------------------------------------------------------------------------- /app/Epiphany.SeoMetadata/.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | true 14 | 15 | 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 31 | 32 | 33 | 34 | 35 | $(SolutionDir).nuget 36 | 37 | 38 | 39 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config 40 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config 41 | 42 | 43 | 44 | $(MSBuildProjectDirectory)\packages.config 45 | $(PackagesProjectConfig) 46 | 47 | 48 | 49 | 50 | $(NuGetToolsPath)\NuGet.exe 51 | @(PackageSource) 52 | 53 | "$(NuGetExePath)" 54 | mono --runtime=v4.0.30319 "$(NuGetExePath)" 55 | 56 | $(TargetDir.Trim('\\')) 57 | 58 | -RequireConsent 59 | -NonInteractive 60 | 61 | "$(SolutionDir) " 62 | "$(SolutionDir)" 63 | 64 | 65 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) 66 | $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols 67 | 68 | 69 | 70 | RestorePackages; 71 | $(BuildDependsOn); 72 | 73 | 74 | 75 | 76 | $(BuildDependsOn); 77 | BuildPackage; 78 | 79 | 80 | 81 | 82 | 83 | 84 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | require('load-grunt-tasks')(grunt); 3 | require('time-grunt')(grunt); 4 | require('grunt-karma')(grunt); 5 | 6 | //cant load this with require 7 | grunt.loadNpmTasks('grunt-contrib-jshint'); 8 | 9 | if (grunt.option('target') && !grunt.file.isDir(grunt.option('target'))) { 10 | grunt.fail.warn('The --target option specified is not a valid directory'); 11 | } 12 | 13 | grunt.initConfig({ 14 | pkg: grunt.file.readJSON('package.json'), 15 | dest: grunt.option('target') || 'dist', 16 | basePath: 'App_Plugins/<%= pkg.name %>', 17 | 18 | concat: { 19 | dist: { 20 | src: [ 21 | 'app/scripts/filters/ellipsisLimit.filter.js', 22 | 'app/scripts/directives/maxlen.directive.js', 23 | 'app/scripts/controllers/metadata.controller.js' 24 | ], 25 | dest: '<%= dest %>/<%= basePath %>/js/epiphany.seo.metadata.js', 26 | nonull: true 27 | } 28 | }, 29 | 30 | sass: { 31 | dist: { 32 | options: { 33 | paths: ['app/styles'], 34 | }, 35 | files: { 36 | '<%= dest %>/<%= basePath %>/css/epiphany.seo.metadata.css': 'app/styles/epiphany.seo.metadata.scss', 37 | } 38 | } 39 | }, 40 | 41 | watch: { 42 | options: { 43 | atBegin: true 44 | }, 45 | 46 | less: { 47 | files: ['app/styles/**/*.less'], 48 | tasks: ['less:dist'] 49 | }, 50 | 51 | js: { 52 | files: ['app/scripts/**/*.js'], 53 | tasks: ['concat:dist'] 54 | }, 55 | 56 | testControllers: { 57 | files: ['app/scripts/**/*.controller.js', 'test/specs/**/*.spec.js'], 58 | tasks: ['jshint', 'test'] 59 | }, 60 | 61 | html: { 62 | files: ['app/views/**/*.html'], 63 | tasks: ['copy:views'] 64 | }, 65 | 66 | config: { 67 | files: ['config/package.manifest'], 68 | tasks: ['copy:config'] 69 | } 70 | }, 71 | 72 | copy: { 73 | config: { 74 | src: 'config/package.manifest', 75 | dest: '<%= dest %>/<%= basePath %>/package.manifest', 76 | }, 77 | 78 | views: { 79 | expand: true, 80 | cwd: 'app/views/', 81 | src: '**', 82 | dest: '<%= dest %>/<%= basePath %>/views/' 83 | }, 84 | 85 | nuget: { 86 | files: [ 87 | { 88 | cwd: '<%= dest %>', 89 | src: ['**/*', '!bin', '!bin/*'], 90 | dest: 'tmp/nuget/content', 91 | expand: true 92 | }, 93 | { 94 | cwd: '<%= dest %>/bin', 95 | src: ['*.dll'], 96 | dest: 'tmp/nuget/lib/net40', 97 | expand: true 98 | } 99 | ] 100 | }, 101 | 102 | umbraco: { 103 | expand: true, 104 | cwd: '<%= dest %>/', 105 | src: '**', 106 | dest: 'tmp/umbraco/' 107 | }, 108 | 109 | testAssets: { 110 | expand: true, 111 | cwd: '<%= dest %>', 112 | src: ['js/umbraco.*.js', 'lib/**/*.js'], 113 | dest: 'test/assets/' 114 | }, 115 | 116 | dll: { 117 | cwd: 'app/Epiphany.SeoMetadata/bin/Release/', 118 | src: 'Epiphany.SeoMetadata.dll', 119 | dest: '<%= dest %>/bin/', 120 | expand: true 121 | }, 122 | }, 123 | 124 | template: { 125 | nuspec: { 126 | options: { 127 | data: { 128 | name: '<%= pkg.name %>', 129 | version: '<%= pkg.version %>', 130 | author: '<%= pkg.author.name %>', 131 | description: '<%= pkg.description %>' 132 | } 133 | }, 134 | files: { 135 | 'tmp/nuget/<%= pkg.name %>.nuspec': 'config/package.nuspec' 136 | } 137 | } 138 | }, 139 | 140 | mkdir: { 141 | pkg: { 142 | options: { 143 | create: ['pkg/nuget', 'pkg/umbraco'] 144 | }, 145 | }, 146 | }, 147 | 148 | nugetpack: { 149 | dist: { 150 | src: 'tmp/nuget/<%= pkg.name %>.nuspec', 151 | dest: 'pkg/nuget/' 152 | } 153 | }, 154 | 155 | umbracoPackage: { 156 | dist: { 157 | src: 'tmp/umbraco', // Path to a folder containing the files to be packaged 158 | dest: 'pkg/umbraco', // Path to a folder to create the package file 159 | options: { 160 | name: '<%= pkg.name %>', 161 | version: '<%= pkg.version %>', 162 | url: '<%= pkg.repository.url %>', 163 | license: '<%= pkg.license %>', 164 | licenseUrl: '<%= pkg.licenseUrl %>', 165 | author: '<%= pkg.author.name %>', 166 | authorUrl: '<%= pkg.author.url %>', 167 | manifest: 'config/package.xml', 168 | readme: 'README.md', 169 | sourceDir: 'tmp/umbraco', 170 | outputDir: 'pkg/umbraco', 171 | } 172 | } 173 | }, 174 | 175 | clean: { 176 | tmp: 'tmp', 177 | dist: 'dist', 178 | test: 'test/assets' 179 | }, 180 | 181 | karma: { 182 | unit: { 183 | configFile: 'test/karma.conf.js' 184 | } 185 | }, 186 | 187 | jshint: { 188 | dev: { 189 | files: { 190 | src: ['app/scripts/**/*.js'] 191 | }, 192 | options: { 193 | curly: true, 194 | eqeqeq: true, 195 | immed: true, 196 | latedef: true, 197 | newcap: true, 198 | noarg: true, 199 | sub: true, 200 | boss: true, 201 | eqnull: true, 202 | //NOTE: we need to use eval sometimes so ignore it 203 | evil: true, 204 | //NOTE: we need to check for strings such as "javascript:" so don't throw errors regarding those 205 | scripturl: true, 206 | //NOTE: we ignore tabs vs spaces because enforcing that causes lots of errors depending on the text editor being used 207 | smarttabs: true, 208 | globals: {} 209 | } 210 | } 211 | }, 212 | 213 | assemblyinfo: { 214 | options: { 215 | files: ['app/Epiphany.SeoMetadata/Epiphany.SeoMetadata.csproj'], 216 | filename: 'AssemblyInfo.cs', 217 | info: { 218 | version: '<%= (pkg.version.indexOf("-") > 0 ? pkg.version.substring(0, pkgMeta.version.indexOf("-")) : pkg.version) %>', 219 | fileVersion: '<%= pkg.version %>' 220 | } 221 | } 222 | }, 223 | 224 | msbuild: { 225 | options: { 226 | stdout: true, 227 | verbosity: 'quiet', 228 | maxCpuCount: 4, 229 | version: 4.0, 230 | buildParameters: { 231 | WarningLevel: 2, 232 | NoWarn: 1607 233 | } 234 | }, 235 | dist: { 236 | src: ['app/Epiphany.SeoMetadata/Epiphany.SeoMetadata.csproj'], 237 | options: { 238 | projectConfiguration: 'Release', 239 | targets: ['Clean', 'Rebuild'], 240 | } 241 | } 242 | }, 243 | 244 | /* 245 | See https://github.com/vojtajina/grunt-bump 246 | 247 | For bumping a version, best to do the following 248 | grunt bump-only:minor 249 | grunt assemblyinfo 250 | grunt bump-commit 251 | */ 252 | 253 | bump: { 254 | options: { 255 | files: ['package.json'], 256 | updateConfigs: [], 257 | commit: true, 258 | commitMessage: 'Release v%VERSION%', 259 | commitFiles: ['package.json', 'app/Epiphany.SeoMetadata/Properties/AssemblyInfo.cs'], 260 | createTag: true, 261 | tagName: 'v%VERSION%', 262 | tagMessage: 'Version %VERSION%', 263 | push: true, 264 | pushTo: 'origin', 265 | gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d', 266 | globalReplace: false, 267 | prereleaseName: 'pre', 268 | regExp: false 269 | } 270 | } 271 | 272 | }); 273 | 274 | grunt.registerTask('default', ['jshint', 'concat', 'sass', 'assemblyinfo', 'msbuild', 'copy:config', 'copy:views', 'copy:dll']); 275 | grunt.registerTask('nuget', ['clean', 'default', 'copy:nuget', 'template:nuspec', 'mkdir:pkg', 'nugetpack']); 276 | grunt.registerTask('package', ['clean', 'default', 'copy:umbraco', 'mkdir:pkg', 'umbracoPackage']); 277 | 278 | grunt.registerTask('test', 'Clean, copy test assets, test', function () { 279 | var assetsDir = grunt.config.get('dest'); 280 | //copies over umbraco assets from --target, this must point at the /umbraco/ directory 281 | if (assetsDir !== 'dist') { 282 | grunt.task.run(['clean:test', 'copy:testAssets', 'karma']); 283 | } else if (grunt.file.isDir('test/assets/js/')) { 284 | grunt.log.oklns('Test assets found, running tests'); 285 | grunt.task.run(['karma']); 286 | } else { 287 | grunt.log.errorlns('Tests assets not found, skipping tests'); 288 | } 289 | }); 290 | }; 291 | -------------------------------------------------------------------------------- /app/Epiphany.SeoMetadata/Epiphany.SeoMetadata.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {56046752-1E1B-420B-B1D1-DB47BE67FCD0} 8 | Library 9 | Properties 10 | Epiphany.SeoMetadata 11 | Epiphany.SeoMetadata 12 | v4.5 13 | 512 14 | .\ 15 | true 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | packages\AutoMapper.3.0.0\lib\net40\AutoMapper.dll 37 | True 38 | 39 | 40 | packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll 41 | True 42 | 43 | 44 | packages\UmbracoCms.Core.7.1.8\lib\businesslogic.dll 45 | True 46 | 47 | 48 | packages\ClientDependency.1.8.2.1\lib\net45\ClientDependency.Core.dll 49 | True 50 | 51 | 52 | packages\ClientDependency-Mvc.1.7.0.4\lib\ClientDependency.Core.Mvc.dll 53 | True 54 | 55 | 56 | packages\UmbracoCms.Core.7.1.8\lib\cms.dll 57 | True 58 | 59 | 60 | packages\UmbracoCms.Core.7.1.8\lib\controls.dll 61 | True 62 | 63 | 64 | packages\xmlrpcnet.2.5.0\lib\net20\CookComputing.XmlRpcV2.dll 65 | True 66 | 67 | 68 | packages\Examine.0.1.57.2941\lib\Examine.dll 69 | True 70 | 71 | 72 | packages\HtmlAgilityPack.1.4.6\lib\Net45\HtmlAgilityPack.dll 73 | True 74 | 75 | 76 | packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll 77 | True 78 | 79 | 80 | packages\ImageProcessor.1.9.5.0\lib\ImageProcessor.dll 81 | True 82 | 83 | 84 | packages\ImageProcessor.Web.3.3.0.0\lib\net45\ImageProcessor.Web.dll 85 | True 86 | 87 | 88 | packages\UmbracoCms.Core.7.1.8\lib\interfaces.dll 89 | True 90 | 91 | 92 | packages\UmbracoCms.Core.7.1.8\lib\log4net.dll 93 | True 94 | 95 | 96 | packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll 97 | True 98 | 99 | 100 | packages\UmbracoCms.Core.7.1.8\lib\Microsoft.ApplicationBlocks.Data.dll 101 | True 102 | 103 | 104 | packages\UmbracoCms.Core.7.1.8\lib\Microsoft.Web.Helpers.dll 105 | True 106 | 107 | 108 | packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll 109 | True 110 | 111 | 112 | packages\Microsoft.AspNet.Mvc.FixedDisplayModes.1.0.1\lib\net40\Microsoft.Web.Mvc.FixedDisplayModes.dll 113 | True 114 | 115 | 116 | packages\MiniProfiler.2.1.0\lib\net40\MiniProfiler.dll 117 | True 118 | 119 | 120 | packages\MySql.Data.6.6.5\lib\net40\MySql.Data.dll 121 | True 122 | 123 | 124 | packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll 125 | True 126 | 127 | 128 | packages\UmbracoCms.Core.7.1.8\lib\SQLCE4Umbraco.dll 129 | True 130 | 131 | 132 | 133 | 134 | 135 | packages\UmbracoCms.Core.7.1.8\lib\System.Data.SqlServerCe.dll 136 | True 137 | 138 | 139 | packages\UmbracoCms.Core.7.1.8\lib\System.Data.SqlServerCe.Entity.dll 140 | True 141 | 142 | 143 | packages\Microsoft.AspNet.WebApi.Client.4.0.30506.0\lib\net40\System.Net.Http.Formatting.dll 144 | True 145 | 146 | 147 | 148 | packages\Microsoft.AspNet.WebPages.2.0.20710.0\lib\net40\System.Web.Helpers.dll 149 | True 150 | 151 | 152 | packages\Microsoft.AspNet.WebApi.Core.4.0.30506.0\lib\net40\System.Web.Http.dll 153 | True 154 | 155 | 156 | packages\Microsoft.AspNet.WebApi.WebHost.4.0.30506.0\lib\net40\System.Web.Http.WebHost.dll 157 | True 158 | 159 | 160 | packages\Microsoft.AspNet.Mvc.4.0.30506.0\lib\net40\System.Web.Mvc.dll 161 | True 162 | 163 | 164 | packages\Microsoft.AspNet.Razor.2.0.20710.0\lib\net40\System.Web.Razor.dll 165 | True 166 | 167 | 168 | packages\Microsoft.AspNet.WebPages.2.0.20710.0\lib\net40\System.Web.WebPages.dll 169 | True 170 | 171 | 172 | packages\Microsoft.AspNet.WebPages.2.0.20710.0\lib\net40\System.Web.WebPages.Deployment.dll 173 | True 174 | 175 | 176 | packages\Microsoft.AspNet.WebPages.2.0.20710.0\lib\net40\System.Web.WebPages.Razor.dll 177 | True 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | packages\UmbracoCms.Core.7.1.8\lib\TidyNet.dll 186 | True 187 | 188 | 189 | packages\UmbracoCms.Core.7.1.8\lib\umbraco.dll 190 | True 191 | 192 | 193 | packages\UmbracoCms.Core.7.1.8\lib\Umbraco.Core.dll 194 | True 195 | 196 | 197 | packages\UmbracoCms.Core.7.1.8\lib\umbraco.DataLayer.dll 198 | True 199 | 200 | 201 | packages\UmbracoCms.Core.7.1.8\lib\umbraco.editorControls.dll 202 | True 203 | 204 | 205 | packages\UmbracoCms.Core.7.1.8\lib\umbraco.MacroEngines.dll 206 | True 207 | 208 | 209 | packages\UmbracoCms.Core.7.1.8\lib\umbraco.providers.dll 210 | True 211 | 212 | 213 | packages\UmbracoCms.Core.7.1.8\lib\Umbraco.Web.UI.dll 214 | True 215 | 216 | 217 | packages\UmbracoCms.Core.7.1.8\lib\umbraco.XmlSerializers.dll 218 | True 219 | 220 | 221 | packages\UmbracoCms.Core.7.1.8\lib\UmbracoExamine.dll 222 | True 223 | 224 | 225 | packages\UmbracoCms.Core.7.1.8\lib\UrlRewritingNet.UrlRewriter.dll 226 | True 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 245 | 246 | 247 | 248 | 255 | --------------------------------------------------------------------------------