My Page Title
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ...
25 | ```
26 |
27 | # Version 5 changes
28 |
29 | Seotamic v5.\* is compatible with Statamic v5.0+.
30 |
31 | # Version 4.1 changes
32 |
33 | Seotamic v4.1 is a minor update that adds related meta tags. The related meta tags are used to link to other pages that are related to the current page in a multisite scenario. They are available as tags and can be used in the Antlers or Blade templates. In the PRO query mode, the related meta tags are also available in the REST API and GraphQL.
34 |
35 | # Version 4 changes
36 |
37 | Statamic v4 compatibility. Some internal functions were changed due to how blueprints work in Statamic v4. This release breaks compatibility with Statamic v3. Upgrade to Statamic v4 before upgrading to this version. Upgrade path from SEOtamic v2 is still the same.
38 |
39 | # Version 3 changes
40 |
41 | Version 3 has breaking changes. If you update from version 1 or 2, your global settings will not be transfered. The data layout is a bit different and so is the data on specific entries.
42 |
43 | The string tags were changed to arrays so to access the data you need to use the `:meta` or `:social` prefixes. For example `{{ seotamic:title }}` becomes `{{ seotamic:meta:title }}`.
44 |
45 | ## Migration from version 2
46 |
47 | If you are migrating from version 2, you can use the following command to migrate your data:
48 |
49 | ```sh
50 | php artisan seotamic:migrate
51 | ```
52 |
53 | This will migrate your global settings and all the entries. It will also remove the old fields from the entries. Make sure to backup your data before running the command.
54 |
55 | # Installation
56 |
57 | Include the package with composer:
58 |
59 | ```sh
60 | composer require cnj/seotamic
61 | ```
62 |
63 | The package requires Laravel 9+ and PHP 8.1+. It will auto register.
64 |
65 | The SEO & Social section tab will appear on all collection entries automatically.
66 |
67 | ## Configuration (optional)
68 |
69 | You can override the default options by publishing the configuration:
70 |
71 | ```
72 | php artisan vendor:publish --provider="Cnj\Seotamic\ServiceProvider" --tag=config
73 | ```
74 |
75 | This will copy the default config file to `config/seotamic.php'.
76 |
77 | If you need to change the default assets container, make sure to apply the change in the Blueprints as well.
78 |
79 | # Usage
80 |
81 | Usage is fairly simple and straight forward. You can visit the global Settings by following the Seotamic link on the navigation in the CP. Make sure to follow the instructions on each field.
82 |
83 | 
84 |
85 | After this you can fine tune the output of each collection entry by editing the SEO settings under the entry's SEO tab. From version 3 you can also preview the output of the meta and social settings. The previews should give an accurate representation of the output.
86 |
87 | 
88 |
89 | ## Antlers
90 |
91 | There are several antler tags available, the easiest is to just include the do everything base tag in the head of your layout:
92 |
93 | ```
94 | {{ seotamic }}
95 | ```
96 |
97 | If you need more control you can manually get each part of the output by using:
98 |
99 | ```
100 | {{ seotamic:meta:title }}
101 | {{ seotamic:meta:description }}
102 | {{ seotamic:meta:canonical }}
103 | {{ seotamic:meta:robots }}
104 | {{ seotamic:meta:related }}
105 | ```
106 |
107 | This will return strings, so you need to wrap them in the appropriate tags, ie:
108 |
109 | ```html
110 | {{ seotamic:meta:title }}
111 | ```
112 |
113 | Social ones will still return everything with these tags (the general one returns this as well)
114 |
115 | ```
116 | {{ seotamic:og }}
117 | {{ seotamic:twitter }}
118 | ```
119 |
120 | If you need more control you can manually get each part of the output by using:
121 |
122 | ```
123 | {{ seotamic:social:title }}
124 | {{ seotamic:social:description }}
125 | {{ seotamic:social:image }}
126 | {{ seotamic:social:site_name }}
127 | ```
128 |
129 | This will return strings, so you need to wrap them in the appropriate tags.
130 |
131 | ## Blade
132 |
133 | Similarly to the Antlers usage, you can use the same tags using Blade:
134 |
135 | ```php
136 | {!! Statamic::tag('seotamic')->context(get_defined_vars()) !!}
137 | ```
138 |
139 | It works similary to the Antlers tags, so you can use single values as well.
140 |
141 | ## Headless (PRO)
142 |
143 | Headless use is straightforward. If using the REST API or GraphQL, the entry will include three Seotamic fields: `seotamic_meta`, `seotamic_social` with the prefilled SEO data.
144 |
145 | You can also set the base canonical url in the config file by setting a value for `headless_mode`. The default value is `false`, but setting it to `https://mydomain.com` will change all canonical urls. The images will NOT use this base url, as they are still served from Statamic.
146 |
147 | **Headless usage is supported only for the PRO version.**
148 |
149 | ## Sitemap (PRO)
150 |
151 | Sitemap is autogenerated under the `/sitemap.xml` url if the sitemap config options are set to true (default).
152 |
153 | **Sitemap is supported only for the PRO version.**
154 |
155 | ## Dynamic OG Image injection
156 |
157 | In projects where you want the OG Image to be dynamic, for
158 | now you can use this ViewModel and inject it to your collection in order to
159 | dynamically assign the OG Image.
160 |
161 | ```php
162 | cascade->get('seotamic_social');
173 |
174 | if ($social) {
175 | return [ ...$social, 'image' => 'https://myimageurl.com/image.jpg', ];
176 | }
177 |
178 | return [];
179 | }
180 | }
181 | ```
182 |
183 | In the example above we use a hardcoded image url which you can change to suit your usecase. Then in your collections you just have to inject the ViewModel.
184 |
185 | ```yaml
186 | title: Posts
187 | inject:
188 | view_model: App\ViewModels\OgImage
189 | ```
190 |
191 | # Credits
192 |
193 | This package was built by [CNJ Digital](https://www.cnj.si/).
194 |
195 | # Version 3, 4 and 5 License
196 |
197 | Version 3 and 4 are a commercial addon for Statamic. It is open source but not free to use. You can purchase a license at [Statamic Marketplace](https://statamic.com/marketplace/addons/seotamic).
198 |
199 | # Version 2 and 1 License
200 |
201 | Version 2 and 1 were licensed under the MIT License.
202 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cnj/seotamic",
3 | "description": "Simple SEO addon for Statamic",
4 | "type": "statamic-addon",
5 | "license": "proprietary",
6 | "require": {
7 | "php": "^8.2",
8 | "statamic/cms": "^5.0.0"
9 | },
10 | "require-dev": {
11 | "orchestra/testbench": "^9.0"
12 | },
13 | "autoload-dev": {
14 | "psr-4": {
15 | "Cnj\\Seotamic\\Tests\\": "tests"
16 | }
17 | },
18 | "autoload": {
19 | "psr-4": {
20 | "Cnj\\Seotamic\\": "src"
21 | }
22 | },
23 | "authors": [
24 | {
25 | "name": "Martin Koterle",
26 | "email": "martin@cnj.si"
27 | }
28 | ],
29 | "extra": {
30 | "statamic": {
31 | "name": "Seotamic",
32 | "description": "Simple SEO addon for Statamic",
33 | "editions": [
34 | "lite",
35 | "pro"
36 | ]
37 | },
38 | "laravel": {
39 | "providers": [
40 | "Cnj\\Seotamic\\ServiceProvider"
41 | ]
42 | }
43 | },
44 | "config": {
45 | "allow-plugins": {
46 | "pixelfear/composer-dist-plugin": true
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/config/seotamic.php:
--------------------------------------------------------------------------------
1 | 'seotamic',
9 |
10 | // Social images asset container. It must exist in Statamic.
11 | // A sensible default is set.
12 | 'container' => 'assets',
13 |
14 | // A list of blueprints where the SEO fields will not be injected.
15 | // By default this list is empty.
16 | 'ignore_blueprints' => [],
17 |
18 | // Do we want to add a /sitemap.xml to the site?
19 | 'sitemap' => true,
20 |
21 | // If set (ie https://test.domain.com) this base url will replace the
22 | // app url for canonical and social url tags. Image links will still
23 | // be linked to the CMS domain.
24 | //
25 | // Defautl value: false
26 | 'headless_mode' => false,
27 |
28 | // Default recommended field lengths.
29 | // They are just guidelines and can be ignored on the frontend
30 | 'meta_title_length' => 60,
31 | 'meta_description_length' => 160,
32 | 'social_title_length' => 60,
33 | 'social_description_length' => 60,
34 |
35 | // Key value store of blueprint names and the field name that contains the
36 | // social image. This is used to override the default social image behaviour
37 | // WARNING: API not final, subject to change in the future, do not use in production
38 | 'social_image_override' => [
39 | 'sample_blueprint' => "sample_field_name"
40 | ],
41 | ];
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "vite",
5 | "build": "vite build"
6 | },
7 | "devDependencies": {
8 | "@rollup/plugin-inject": "^5.0.5",
9 | "@vitejs/plugin-vue2": "^2.2.0",
10 | "autoprefixer": "^10.4.19",
11 | "laravel-vite-plugin": "^1.2.0",
12 | "postcss": "^8.4.23",
13 | "tailwindcss": "^3.3.2",
14 | "vite": "^6.2.6"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/resources/css/cp.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/resources/dist/build/assets/cp-BbkQ3t6Y.css:
--------------------------------------------------------------------------------
1 | *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.seotamic-mb-0\.5{margin-bottom:.125rem}.seotamic-mb-2{margin-bottom:.5rem}.seotamic-mt-2{margin-top:.5rem}.seotamic-mt-8{margin-top:2rem}.seotamic-mt-\[3px\]{margin-top:3px}.seotamic-mt-\[5px\]{margin-top:5px}.seotamic-line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.seotamic-flex{display:flex}.seotamic-h-\[261px\]{height:261px}.seotamic-h-auto{height:auto}.seotamic-h-px{height:1px}.seotamic-w-full{width:100%}.seotamic-max-w-\[500px\]{max-width:500px}.seotamic-max-w-\[600px\]{max-width:600px}.seotamic-flex-col{flex-direction:column}.seotamic-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.seotamic-rounded-\[3px\]{border-radius:3px}.seotamic-border{border-width:1px}.seotamic-border-\[\#dadde1\]{--tw-border-opacity: 1;border-color:rgb(218 221 225 / var(--tw-border-opacity, 1))}.seotamic-bg-\[\#f2f3f5\]{--tw-bg-opacity: 1;background-color:rgb(242 243 245 / var(--tw-bg-opacity, 1))}.seotamic-bg-blue-300{--tw-bg-opacity: 1;background-color:rgb(147 197 253 / var(--tw-bg-opacity, 1))}.seotamic-bg-gray-300{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1))}.seotamic-bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.seotamic-p-3{padding:.75rem}.\!seotamic-px-3{padding-left:.75rem!important;padding-right:.75rem!important}.\!seotamic-py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.seotamic-px-3{padding-left:.75rem;padding-right:.75rem}.seotamic-py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.seotamic-text-\[13px\]{font-size:13px}.seotamic-text-base{font-size:1rem;line-height:1.5rem}.seotamic-text-sm{font-size:.875rem;line-height:1.25rem}.seotamic-text-xl{font-size:1.25rem;line-height:1.75rem}.seotamic-text-xs{font-size:.75rem;line-height:1rem}.seotamic-font-bold{font-weight:700}.seotamic-font-semibold{font-weight:600}.seotamic-uppercase{text-transform:uppercase}.seotamic-leading-\[18px\]{line-height:18px}.seotamic-leading-\[20px\]{line-height:20px}.seotamic-leading-\[26px\]{line-height:26px}.seotamic-leading-none{line-height:1}.seotamic-tracking-wider{letter-spacing:.05em}.seotamic-text-\[\#1a0dab\]{--tw-text-opacity: 1;color:rgb(26 13 171 / var(--tw-text-opacity, 1))}.seotamic-text-\[\#1c2e36\]{--tw-text-opacity: 1;color:rgb(28 46 54 / var(--tw-text-opacity, 1))}.seotamic-text-\[\#1d2129\]{--tw-text-opacity: 1;color:rgb(29 33 41 / var(--tw-text-opacity, 1))}.seotamic-text-\[\#202124\]{--tw-text-opacity: 1;color:rgb(32 33 36 / var(--tw-text-opacity, 1))}.seotamic-text-\[\#4d5156\]{--tw-text-opacity: 1;color:rgb(77 81 86 / var(--tw-text-opacity, 1))}.seotamic-text-\[\#606770\]{--tw-text-opacity: 1;color:rgb(96 103 112 / var(--tw-text-opacity, 1))}.seotamic-text-\[\#737f8c\]{--tw-text-opacity: 1;color:rgb(115 127 140 / var(--tw-text-opacity, 1))}.seotamic-shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}
2 |
--------------------------------------------------------------------------------
/resources/dist/build/assets/cp-D84CPrby.js:
--------------------------------------------------------------------------------
1 | function s(i,t,e,a,l,o,u,p){var n=typeof i=="function"?i.options:i;return t&&(n.render=t,n.staticRenderFns=e,n._compiled=!0),{exports:i,options:n}}const _={props:{title:{type:String,required:!0,default:""}}};var h=function(){var t=this,e=t._self._c;return e("div",[e("div",{staticClass:"seotamic-text-sm seotamic-font-bold seotamic-text-[#1c2e36]"},[t._v(" "+t._s(t.title)+" ")]),e("div",{staticClass:"seotamic-text-[13px] seotamic-text-[#737f8c] seotamic-mb-2"},[t._t("default")],2)])},f=[],x=s(_,h,f);const c=x.exports,g={props:{previewTitle:{type:String,required:!0,default:""},permalink:{type:String,required:!1,default:""},domain:{type:String,required:!0,default:""},title:{type:String,required:!0,default:""},description:{type:String,required:!0,default:""}},methods:{truncate(i,t=160){return!i||i.length<=t?i:i.slice(0,t)+" …"}},computed:{url(){return this.domain&&(this.domain.startsWith("https://")||this.domain.startsWith("http://"))?this.domain:"https://"+this.domain}}};var b=function(){var t=this,e=t._self._c;return e("div",[t.previewTitle?e("div",{staticClass:"seotamic-text-xs seotamic-uppercase seotamic-font-bold seotamic-tracking-wider"},[t._v(" "+t._s(t.previewTitle)+" ")]):t._e(),t.permalink?e("div",{staticClass:"seotamic-mt-2"},[e("a",{staticClass:"text-sm underline text-blue hover:text-blue-dark",attrs:{href:"https://pagespeed.web.dev/report?url="+t.permalink,target:"_blank",rel:"noopener noreferrer"}},[t._v(" Pagespeed Insights ")])]):t._e(),e("div",{staticClass:"seotamic-mt-2 seotamic-bg-white seotamic-border seotamic-shadow-sm seotamic-p-3 seotamic-flex seotamic-flex-col seotamic-max-w-[600px] seotamic-rounded-[3px]"},[e("div",{staticClass:"seotamic-text-sm seotamic-text-[#202124] seotamic-mb-0.5"},[t._v(" "+t._s(t.url)+" ")]),e("div",{staticClass:"seotamic-text-[#1a0dab] seotamic-text-xl seotamic-leading-[26px] seotamic-truncate seotamic-mt-[5px]"},[t._v(" "+t._s(t.title)+" ")]),e("div",{staticClass:"seotamic-text-sm seotamic-text-[#4d5156]"},[t._v(" "+t._s(t.truncate(t.description))+" ")])])])},y=[],w=s(g,b,y);const m=w.exports,$={components:{Heading:c,SearchPreview:m},computed:{prependExists(){return this.$attrs.meta.seotamic.title_prepend!==null},appendExists(){return this.$attrs.meta.seotamic.title_append!==null},previewTitle(){const i=this.prependExists?this.$attrs.meta.seotamic.title_prepend+" ":"",t=this.appendExists?" "+this.$attrs.meta.seotamic.title_append:"";return`${i}${this.$attrs.meta.t.demo_title}${t}`}}};var C=function(){var t=this,e=t._self._c;return e("SearchPreview",{attrs:{domain:t.$attrs.meta.seotamic.preview_domain,title:t.previewTitle,description:t.$attrs.meta.t.default_description}})},S=[],k=s($,C,S);const D=k.exports,T={props:{previewTitle:{type:String,required:!1,default:""},permalink:{type:String,required:!1,default:""},domain:{type:String,required:!0,default:""},image:{type:String,required:!0,default:""},title:{type:String,required:!0,default:""},description:{type:String,required:!0,default:""}},data(){return{showDescription:!0}},watch:{title(){this.$nextTick(()=>{this.$refs.title.clientHeight>=40?this.showDescription=!1:this.showDescription=!0})}}};var F=function(){var t=this,e=t._self._c;return e("div",{},[t.previewTitle?e("div",{staticClass:"seotamic-text-xs seotamic-uppercase seotamic-font-bold seotamic-tracking-wider"},[t._v(" "+t._s(t.previewTitle)+" ")]):t._e(),t.permalink?e("div",{staticClass:"seotamic-mt-2"},[e("a",{staticClass:"text-sm underline text-blue hover:text-blue-dark",attrs:{href:"https://developers.facebook.com/tools/debug/?q="+t.permalink,target:"_blank",rel:"noopener noreferrer"}},[t._v(" Facebook Debugger ")])]):t._e(),e("div",{staticClass:"seotamic-mt-2 seotamic-bg-[#f2f3f5] seotamic-border seotamic-border-[#dadde1] seotamic-shadow-sm seotamic-flex seotamic-flex-col seotamic-max-w-[500px] seotamic-rounded-[3px]"},[e("div",{staticClass:"seotamic-h-[261px] seotamic-w-full seotamic-bg-blue-300 relative"},[t.image?e("img",{staticClass:"absolute object-cover w-full h-full",attrs:{src:t.image}}):t._e()]),e("div",{staticClass:"seotamic-py-2.5 seotamic-px-3"},[e("div",{staticClass:"seotamic-text-xs seotamic-text-[#606770] seotamic-leading-none seotamic-uppercase"},[t._v(" "+t._s(t.domain)+" ")]),e("div",{ref:"title",staticClass:"seotamic-text-[#1d2129] seotamic-font-semibold seotamic-text-base seotamic-leading-[20px] seotamic-line-clamp-2 seotamic-mt-[5px]"},[t._v(" "+t._s(t.title)+" ")]),e("div",{directives:[{name:"show",rawName:"v-show",value:t.showDescription,expression:"showDescription"}],staticClass:"seotamic-text-sm seotamic-leading-[18px] seotamic-text-[#606770] seotamic-truncate seotamic-mt-[3px]"},[t._v(" "+t._s(t.description)+" ")])])])])},q=[],R=s(T,F,q);const d=R.exports,O={components:{SocialPreview:d},mixins:[Fieldtype]};var P=function(){var t=this,e=t._self._c;return e("SocialPreview",{attrs:{domain:t.meta.seotamic.preview_domain,title:t.meta.seotamic.social_title,image:t.meta.image,description:t.meta.seotamic.social_description}})},E=[],H=s(O,P,E);const B=H.exports,G={props:{value:{type:String,required:!0},options:{type:Array,required:!0,default:()=>[]}}};var I=function(){var t=this,e=t._self._c;return e("div",{staticClass:"button-group-fieldtype-wrapper inline-mode"},[e("div",{staticClass:"btn-group"},t._l(t.options,function(a,l){return e("button",{key:l,ref:"button",refInFor:!0,staticClass:"btn !seotamic-py-1 seotamic-text-sm seotamic-h-auto !seotamic-px-3",class:{active:t.value===a.value},attrs:{name:a.label,value:a.value},domProps:{textContent:t._s(a.label||a.value)},on:{click:function(o){return t.$emit("input",a.value)}}})}),0)])},A=[],j=s(G,I,A);const v=j.exports;function r(i,t,e=!1){let a;return function(){const l=this,o=arguments,u=function(){a=null,e||i.apply(l,o)},p=e&&!a;clearTimeout(a),a=setTimeout(u,t),p&&i.apply(l,o)}}const M={components:{ButtonGroup:v,Heading:c,SearchPreview:m},mixins:[Fieldtype],data(){return{description:this.meta.t.default_description,titleOptions:[{label:this.meta.t.label_title,value:"title"},{label:this.meta.t.label_custom,value:"custom"}],descriptionOptions:[{label:this.meta.t.label_empty,value:"empty"},{label:this.meta.t.label_custom,value:"custom"}]}},computed:{prependExists(){return this.meta.seotamic.title_prepend!==null},appendExists(){return this.meta.seotamic.title_append!==null},previewTitle(){const i=this.prependExists&&this.value.title.prepend?this.meta.seotamic.title_prepend+" ":"",t=this.appendExists&&this.value.title.append?" "+this.meta.seotamic.title_append:"";return`${i}${this.value.title.value}${t}`},previewDescription(){return this.value.description.value===""||!this.value.description.value?this.description:this.value.description.value}},watch:{"value.title.type":function(i){!this.value||!this.value.title||(i==="title"?(this.value.title.custom_value=this.value.title.value,this.value.title.value=this.meta.title):this.value.title.value=this.value.title.custom_value)},"value.description.type":function(i,t){!this.value||!this.value.description||(t==="custom"&&(this.value.description.custom_value=this.value.description.value),i==="custom"?this.value.description.value=this.value.description.custom_value:this.value.description.value="")}},created(){this.updateTitleDebounced=r(i=>{!this.value||!this.value.title||(this.value.title.value=i,this.value.title.type==="custom"&&(this.value.title.custom_value=i))},50),this.updateDescriptionDebounced=r(i=>{!this.value||!this.value.description||(this.value.description.value=i,this.value.description.type==="custom"&&(this.value.description.custom_value=i))},50)},methods:{updatePrepend(i){this.value.title.prepend=i},updateAppend(i){this.value.title.append=i}}};var N=function(){var t=this,e=t._self._c;return t.value?e("div",[e("div",[e("Heading",{attrs:{title:t.meta.t.title_title}},[t._v(" "+t._s(t.meta.t.title_instructions)+" ")]),e("ButtonGroup",{attrs:{options:t.titleOptions},model:{value:t.value.title.type,callback:function(a){t.$set(t.value.title,"type",a)},expression:"value.title.type"}}),e("div",{staticClass:"seotamic-mt-2"},[e("text-input",{ref:"title",attrs:{value:t.value.title.value,type:"text",isReadOnly:t.value.title.type!=="custom",limit:t.meta.config.meta_title_length,name:"meta_title",id:"meta_title"},on:{input:t.updateTitleDebounced}})],1),e("div",{staticClass:"seotamic-mt-2"},[e("div",{staticClass:"toggle-fieldtype-wrapper"},[e("toggle-input",{attrs:{value:t.value.title.prepend,"read-only":!t.prependExists},on:{input:t.updatePrepend}}),e("label",{staticClass:"inline-label"},[t._v(" "+t._s(t.meta.t.prepend_label)+" ")])],1)]),e("div",[e("div",{staticClass:"toggle-fieldtype-wrapper"},[e("toggle-input",{attrs:{value:t.value.title.append,"read-only":!t.appendExists},on:{input:t.updateAppend}}),e("label",{staticClass:"inline-label"},[t._v(" "+t._s(t.meta.t.append_label)+" ")])],1)])],1),e("div",{staticClass:"seotamic-mt-8"},[e("Heading",{attrs:{title:t.meta.t.description_title}},[t._v(" "+t._s(t.meta.t.description_instructions)+" ")]),e("ButtonGroup",{attrs:{options:t.descriptionOptions},model:{value:t.value.description.type,callback:function(a){t.$set(t.value.description,"type",a)},expression:"value.description.type"}}),e("div",{staticClass:"seotamic-mt-2"},[e("textarea-input",{ref:"description",attrs:{value:t.value.description.value,type:"text",isReadOnly:t.value.description.type!=="custom",limit:t.meta.config.meta_description_length,name:"meta_description",id:"meta_description"},on:{input:t.updateDescriptionDebounced}})],1)],1),e("SearchPreview",{staticClass:"seotamic-mt-8",attrs:{"preview-title":t.meta.t.preview_title,permalink:t.meta.permalink,domain:t.meta.seotamic.preview_domain,title:t.previewTitle,description:t.previewDescription}}),e("div",{staticClass:"seotamic-mt-8 seotamic-h-px seotamic-bg-gray-300"})],1):t._e()},W=[],z=s(M,N,W);const J=z.exports,K={components:{ButtonGroup:v,Heading:c,SocialPreview:d},mixins:[Fieldtype],data(){return{titleOptions:[{label:this.meta.t.label_title,value:"title"},{label:this.meta.t.label_general,value:"general"},{label:this.meta.t.label_custom,value:"custom"}],descriptionOptions:[{label:this.meta.t.label_meta,value:"meta"},{label:this.meta.t.label_general,value:"general"},{label:this.meta.t.label_custom,value:"custom"}]}},watch:{"value.title.type":function(i,t){!this.value||!this.value.title||(t==="custom"&&(this.value.title.custom_value=this.value.title.value),i==="title"?this.meta.meta.title.type==="custom"?this.value.title.value=this.meta.meta.title.value:this.value.title.value=this.meta.title:i==="general"?this.value.title.value=this.meta.seotamic.social_title:this.value.title.value=this.value.title.custom_value)},"value.description.type":function(i,t){!this.value||!this.value.description||(t==="custom"&&(this.value.description.custom_value=this.value.description.value),i==="meta"?this.meta.meta.description.type==="custom"?this.value.description.value=this.meta.meta.description.value:this.value.description.value=this.meta.seotamic.social_description:i==="general"?this.value.description.value=this.meta.seotamic.social_description:this.value.description.value=this.value.description.custom_value)}},created(){this.updateTitleDebounced=r(i=>{!this.value||!this.value.title||(this.value.title.value=i,this.value.title.type==="custom"&&(this.value.title.custom_value=i))},50),this.updateDescriptionDebounced=r(i=>{!this.value||!this.value.description||(this.value.description.value=i,this.value.description.type==="custom"&&(this.value.description.custom_value=i))},50)}};var L=function(){var t=this,e=t._self._c;return t.value?e("div",[e("div",[e("Heading",{attrs:{title:t.meta.t.title_title}},[t._v(" "+t._s(t.meta.t.title_instructions)+" ")]),e("ButtonGroup",{attrs:{options:t.titleOptions},model:{value:t.value.title.type,callback:function(a){t.$set(t.value.title,"type",a)},expression:"value.title.type"}}),e("div",{staticClass:"seotamic-mt-2"},[e("text-input",{ref:"title",attrs:{value:t.value.title.value,type:"text",isReadOnly:t.value.title.type!=="custom",limit:t.meta.config.social_title_length,name:"og_title",id:"og_title"},on:{input:t.updateTitleDebounced}})],1)],1),e("div",{staticClass:"seotamic-mt-8"},[e("Heading",{attrs:{title:t.meta.t.description_title}},[t._v(" "+t._s(t.meta.t.description_instructions)+" ")]),e("ButtonGroup",{attrs:{options:t.descriptionOptions},model:{value:t.value.description.type,callback:function(a){t.$set(t.value.description,"type",a)},expression:"value.description.type"}}),e("div",{staticClass:"seotamic-mt-2"},[e("text-input",{ref:"description",attrs:{value:t.value.description.value,type:"text",isReadOnly:t.value.description.type!=="custom",limit:t.meta.config.social_description_length,name:"description",id:"description"},on:{input:t.updateDescriptionDebounced}})],1)],1),e("SocialPreview",{staticClass:"seotamic-mt-8",attrs:{"preview-title":t.meta.t.preview_title,permalink:t.meta.permalink,domain:t.meta.seotamic.preview_domain,title:t.value.title.value,image:t.meta.social_image,description:t.value.description.value}}),e("div",{staticClass:"seotamic-mt-8 seotamic-h-px seotamic-bg-gray-300"})],1):t._e()},Q=[],U=s(K,L,Q);const X=U.exports;Statamic.booting(()=>{Statamic.component("seotamic_search_preview-fieldtype",D),Statamic.component("seotamic_social_preview-fieldtype",B),Statamic.component("seotamic_meta-fieldtype",J),Statamic.component("seotamic_social-fieldtype",X)});
2 |
--------------------------------------------------------------------------------
/resources/dist/build/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "resources/css/cp.css": {
3 | "file": "assets/cp-BbkQ3t6Y.css",
4 | "src": "resources/css/cp.css",
5 | "isEntry": true
6 | },
7 | "resources/js/cp.js": {
8 | "file": "assets/cp-D84CPrby.js",
9 | "name": "cp",
10 | "src": "resources/js/cp.js",
11 | "isEntry": true
12 | }
13 | }
--------------------------------------------------------------------------------
/resources/js/components/SeotamicMetaFieldtype.vue:
--------------------------------------------------------------------------------
1 |
2 |
54 |
55 |
56 |
112 |
--------------------------------------------------------------------------------
/resources/js/cp.js:
--------------------------------------------------------------------------------
1 | import SeotamicSearchPreview from "./components/SeotamicSearchPreview.vue";
2 | import SeotamicSocialPreview from "./components/SeotamicSocialPreview.vue";
3 | import SeotamicMetaFieldtype from "./components/SeotamicMetaFieldtype.vue";
4 | import SeotamicSocialFieldtype from "./components/SeotamicSocialFieldtype.vue";
5 |
6 | Statamic.booting(() => {
7 | Statamic.component(
8 | "seotamic_search_preview-fieldtype",
9 | SeotamicSearchPreview
10 | );
11 | Statamic.component(
12 | "seotamic_social_preview-fieldtype",
13 | SeotamicSocialPreview
14 | );
15 | Statamic.component("seotamic_meta-fieldtype", SeotamicMetaFieldtype);
16 | Statamic.component("seotamic_social-fieldtype", SeotamicSocialFieldtype);
17 | });
18 |
--------------------------------------------------------------------------------
/resources/js/helpers/debounce.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creates a debounced function that delays invoking the provided function
3 | * until after 'wait' milliseconds have elapsed since the last time it was invoked.
4 | *
5 | * @param {Function} func - The function to debounce
6 | * @param {number} wait - The number of milliseconds to delay
7 | * @param {boolean} [immediate=false] - Whether to invoke the function on the leading edge instead of the trailing edge
8 | * @returns {Function} - The debounced function
9 | */
10 | export function debounce(func, wait, immediate = false) {
11 | let timeout;
12 |
13 | return function () {
14 | const context = this;
15 | const args = arguments;
16 |
17 | const later = function () {
18 | timeout = null;
19 | if (!immediate) func.apply(context, args);
20 | };
21 |
22 | const callNow = immediate && !timeout;
23 |
24 | clearTimeout(timeout);
25 | timeout = setTimeout(later, wait);
26 |
27 | if (callNow) func.apply(context, args);
28 | };
29 | }
30 |
31 | export default debounce;
32 |
--------------------------------------------------------------------------------
/resources/lang/en/general.php:
--------------------------------------------------------------------------------
1 | 'SEOtamic',
8 | 'intro' => 'Control your SEO general settings here. Make sure to read the instructions on each input. These settings can be overidden on specific entries/pages.',
9 |
10 | 'title' => 'Settings',
11 |
12 | /**
13 | * Title Tab
14 | */
15 | 'title_title' => 'Title',
16 |
17 | 'title_section_title' => 'Title',
18 | 'title_section_instructions' => 'You should always have a unique title tag on every page that describes the page. By default SEOtamic uses the Entry title as the title, which you can edit in the SEO settings for each page.',
19 |
20 | 'title_prepend_title' => 'Prepend on Title',
21 | 'title_prepend_instructions' => 'This will be PREPENDED to all titles throughout the page. Not commonly used.',
22 |
23 | 'title_append_title' => 'Append on Title',
24 | 'title_append_instructions' => 'This will be APPENDED to all titles throughout the page. Commonly used for your brand/product name.',
25 |
26 | 'title_preview_title' => 'Search preview (requires reload on save)',
27 | 'title_preview_placeholder_title' => 'Demo Page Title',
28 | 'title_preview_placeholder_description' => 'This description will be prefilled by the search engine depending on your content. You can change it manually by selecting custom and typing in your own.',
29 |
30 | /**
31 | * Social Tab
32 | */
33 | 'social_title' => 'Social',
34 |
35 | 'social_site_name_title' => 'Site Name',
36 | 'social_site_name_instructions' => 'The name of your site. This might be displayed while sharing on socials. Usually this is the name of your brand/product. This is different than the website title, which is meant for describing the content of a specififc page. Example: Site name: *Apple* Page title: *iPhone 14 Pro specifications*.',
37 |
38 | 'social_info_title' => 'General Social Settings',
39 | 'social_info_instructions' => 'The best practice for social shares is to create unique content for each page. Since this not always an option, it is advised to set this for the most shared landing pages. The information below will be used as a fallback for all other pages.',
40 |
41 | 'social_title_title' => 'Title',
42 | 'social_title_instructions' => 'Focus on accuracy, value, and clickability. Keep it short to prevent overflow. 40 characters for mobile and 60 for desktop is roughly the sweet spot. Use the raw title. Don’t include branding (e.g. your site name).',
43 |
44 | 'social_description_title' => 'Description',
45 | 'social_description_instructions' => 'General Description, can be overridden on specific pages. Complement the title to make the snippet as appealing and click-worthy as possible. Copy your meta description here if it makes sense. Keep it short and sweet. Facebook recommends 2–4 sentences, but that often truncates.',
46 |
47 | 'social_image_title' => 'Share Image',
48 | 'social_image_instructions' => 'Use your logo or any other branded image for the default image. Use images with a 1.91:1 ratio and minimum recommended dimensions of 1200x630 for optimal clarity across all devices.',
49 |
50 | 'social_image_compress_title' => 'Compress & Resize Share Image',
51 | 'social_image_compress_instructions' => 'This will compress and resize the image to 1200x630. This will compress all social share images. Do not use this if you are using a custom image resolution.',
52 |
53 | 'social_preview_title' => 'Social preview (requires reload on save)',
54 |
55 | 'social_open_graph_title' => 'Output Open Graph tags',
56 | 'social_open_graph_instructions' => 'Open Graph tags are used to control how URLs are displayed when shared on social media. If you don\'t have Open Graph tags on your site, Facebook will try to scrape the content of the page to determine what to display. This can lead to issues with how your content is displayed, and it can also lead to Facebook displaying the wrong content.',
57 |
58 | 'social_twitter_title' => 'Output Twitter tags',
59 | 'social_twitter_instructions' => 'Twitter cards are a way to attach rich photos, videos and media experience to Tweets that drive traffic to your website. You can use Twitter cards to help you drive traffic to your website, increase the visibility of your content, and get more engagement with your Tweets.',
60 |
61 | /**
62 | * Settings Tab
63 | */
64 | 'settings_title' => 'Settings',
65 |
66 | 'settings_preview_domain_title' => 'Preview Domain/URL',
67 | 'settings_preview_domain_instructions' => 'This is the domain that will be used to generate the social and SEO previews.',
68 |
69 | 'settings_robots_title' => 'Disable page indexing (noindex, nofollow)',
70 | 'settings_robots_instructions' => 'This will prevent the page from being indexed by search engines. This is useful for pages that are not ready for public viewing, or for pages that you don\'t want to be indexed by search engines This will be set for all pages.',
71 | ];
72 |
--------------------------------------------------------------------------------
/resources/lang/en/seo.php:
--------------------------------------------------------------------------------
1 | 'SEO',
8 |
9 | 'section_meta_title' => 'Meta',
10 | 'section_meta_instructions' => 'Meta tags are essentially little content descriptors that help tell search engines what a web page is about.',
11 |
12 | 'meta_title' => 'SEOtamic Meta',
13 |
14 | 'meta_title_title' => 'Title',
15 | 'meta_title_instructions' => 'Focus on accuracy, value, and clickability. Keep it short to prevent overflow. There’s no official guidance on this, but 40 characters for mobile and 60 for desktop is roughly the sweet spot. Use the raw title. Don’t include branding (e.g., your site name).',
16 | 'meta_prepend_label' => 'Prepend to the title the text set in the SEOtamic settings',
17 | 'meta_append_label' => 'Append to the title the text set in the SEOtamic settings',
18 |
19 | 'meta_description_title' => 'Description',
20 | 'meta_description_instructions' => 'It can be used to determine the text used under the title on search engine results pages. If empty, search engines will generate this text automatically. It is best to leave this empty and let the search engine generate it automatically.',
21 | 'meta_default_description' => 'This description will be prefilled by the search engine depending on your content. You can change it manually by selecting custom and typing in your own.',
22 |
23 | 'meta_label_title' => ' Entry Title',
24 | 'meta_label_auto' => 'Auto',
25 | 'meta_label_custom' => 'Custom',
26 | 'meta_label_empty' => 'Empty',
27 | 'meta_preview_title' => 'Search preview',
28 |
29 | 'canonical_title' => 'Canonical URL',
30 | 'canonical_instructions' => 'Canonical URLs are used to tell search engines which URL is the original source of a page. This is useful when you have multiple URLs that point to the same page. For example, if you have a page that can be accessed with or without a trailing slash, you can use a canonical URL to tell search engines which one is the original. This helps search engines avoid duplicate content issues.',
31 |
32 | 'robots_title' => 'Disable page indexing (noindex, nofollow)',
33 | 'robots_instructions' => 'This will prevent this page from being indexed by search engines. This is useful for pages that are not ready for public viewing, or for pages that you don\'t want to be indexed by search engines.',
34 | ];
35 |
--------------------------------------------------------------------------------
/resources/lang/en/social.php:
--------------------------------------------------------------------------------
1 | 'Social',
9 |
10 | 'section_social_title' => 'Social',
11 | 'section_social_instructions' => 'Social (Open Graph, Twitter, …) is used to display information while sharing the website link. OpenGraph is the most common sharing protocol (used by Facebook, Slack…).',
12 |
13 | 'social_title' => 'SEOtamic Social',
14 |
15 |
16 | 'entry_social_section' => 'Social',
17 | 'entry_social_instructions' => 'Social',
18 |
19 | 'social_field_title_title' => '',
20 | 'social_field_title_instructions' => 'The title should be between 50 and 60 characters and not have any branding.',
21 | 'social_field_description_title' => 'Social description',
22 | 'social_field_description_instructions' => 'Shown below the title. It is used to describe the content of the page. If Meta description is not empty, it can be reused here. Usually the Meta description is longer.',
23 | 'social_field_label_title' => 'Auto',
24 | 'social_field_label_general' => 'General',
25 | 'social_field_label_custom' => 'Custom',
26 | 'social_field_label_meta' => 'Auto',
27 | 'social_field_preview_title' => 'Social preview',
28 |
29 | 'social_image_title' => 'Social Image',
30 | 'social_image_instructions' => 'This image will be used when sharing the page on social networks. If left empty, the default image will be used. Use images with a 1.91:1 ratio and minimum recommended dimensions of 1200x630 for optimal clarity across all devices.',
31 | ];
32 |
--------------------------------------------------------------------------------
/resources/screenshots/meta_preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cnj-digital/seotamic/0917b76a0564f27f40092f11e103d6d0e0032a57/resources/screenshots/meta_preview.png
--------------------------------------------------------------------------------
/resources/screenshots/social_preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cnj-digital/seotamic/0917b76a0564f27f40092f11e103d6d0e0032a57/resources/screenshots/social_preview.png
--------------------------------------------------------------------------------
/resources/views/all.antlers.html:
--------------------------------------------------------------------------------
1 | {{ partial src="seotamic::partials/general" }}
2 | {{ partial src="seotamic::partials/canonical" }}
3 | {{ partial src="seotamic::partials/og" }}
4 | {{ partial src="seotamic::partials/twitter" }}
5 | {{ partial src="seotamic::partials/robots" }}
6 | {{ partial src="seotamic::partials/related" }}
--------------------------------------------------------------------------------
/resources/views/cp/settings.blade.php:
--------------------------------------------------------------------------------
1 | @extends('statamic::layout')
2 | @section('title', 'SEOtamic')
3 |
4 | @section('content')
5 |