Skip to content

Commit 8660593

Browse files
committed
Widget permalink
1 parent 0ee0908 commit 8660593

File tree

5 files changed

+251
-0
lines changed

5 files changed

+251
-0
lines changed

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
assets/

assets/ezgif-5-f5c945ae99.gif

163 KB
Loading

index.js

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
export class Control extends React.Component {
5+
constructor(props) {
6+
super(props);
7+
let decoded = this.decodeSlug(this.props.value);
8+
this.state = {
9+
showEdit: false,
10+
inputVal: decoded,
11+
};
12+
13+
this.toggleEdit = this.toggleEdit.bind(this);
14+
this.saveEdit = this.saveEdit.bind(this);
15+
this.handleChange = this.handleChange.bind(this);
16+
}
17+
18+
slugify(str) {
19+
return str
20+
.normalize('NFD')
21+
.replace(/[\u0300-\u036f]/g, '')
22+
.toLowerCase()
23+
.trim()
24+
.replace(/[^\w^/\s-]/g, '')
25+
.replace(/[\s_-]+/g, '-')
26+
.replace(/^-+|-+$/g, '');
27+
}
28+
29+
isValid = () => {
30+
const { value } = this.state.inputVal;
31+
if (!value) return true;
32+
const parser = document.createElement('a');
33+
parser.href = `http://example.com${this.prepareUrl(value)}/`;
34+
return parser.pathname !== value
35+
? { error: { message: 'Invalid pathname.' } }
36+
: true;
37+
};
38+
39+
decodeSlug(val) {
40+
let prefix = this.props.field.get('prefix', '');
41+
42+
if (prefix) {
43+
val.replace(`/${prefix}/`, '');
44+
}
45+
46+
if (val == '/') {
47+
return '';
48+
}
49+
50+
return val
51+
.split('/')
52+
.filter((n) => n && n !== prefix)
53+
.join('/');
54+
}
55+
56+
toggleEdit() {
57+
this.setState((prev) => {
58+
return {
59+
showEdit: !prev.showEdit,
60+
};
61+
});
62+
}
63+
64+
handleChange = (e) => {
65+
this.setState({ inputVal: e.target.value });
66+
};
67+
68+
prepareUrl(val) {
69+
console.log(val);
70+
let result = val;
71+
let prefix = this.props.field.get('prefix', '');
72+
73+
if (val === '') {
74+
return '/';
75+
}
76+
77+
if (result.charAt(0) !== '/') {
78+
result = `/${result}/`;
79+
}
80+
81+
if (prefix) {
82+
result = `/${prefix}${result}`;
83+
}
84+
85+
return result;
86+
}
87+
88+
saveEdit() {
89+
let slug = this.slugify(this.state.inputVal);
90+
this.setState({
91+
showEdit: false,
92+
inputVal: slug,
93+
});
94+
95+
this.prepareUrl(slug);
96+
97+
this.props.onChange(this.prepareUrl(slug));
98+
}
99+
100+
render() {
101+
const { field, forID, value, classNameWrapper } = this.props;
102+
const url = field.get('url', '');
103+
const prefix = field.get('prefix', '');
104+
105+
return (
106+
<div>
107+
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
108+
<span style={{ fontSize: 14 }}>Permalink:</span>
109+
{this.state.showEdit && (
110+
<div style={{ display: 'flex', alignItems: 'center' }}>
111+
<div
112+
style={{ fontSize: 14, display: 'flex', alignItems: 'center' }}>
113+
{url}/ {prefix && <span>{prefix}/</span>}
114+
</div>
115+
<input
116+
id={forID}
117+
className={classNameWrapper}
118+
style={{ height: 20, padding: 5 }}
119+
value={this.state.inputVal}
120+
onChange={this.handleChange}
121+
/>
122+
<span>/</span>
123+
<button onClick={this.saveEdit}>OK</button>
124+
<button onClick={this.toggleEdit}>Cancel</button>
125+
</div>
126+
)}
127+
{!this.state.showEdit && (
128+
<a
129+
style={{ color: 'blue', textDecoration: 'underline' }}
130+
href={`${url}${value}`}
131+
target='_blank'
132+
rel='noopener noreferrer'>
133+
{url}
134+
{value}
135+
</a>
136+
)}
137+
{!this.state.showEdit && (
138+
<button onClick={this.toggleEdit}>Edit</button>
139+
)}
140+
</div>
141+
</div>
142+
);
143+
}
144+
}
145+
146+
Control.propTypes = {
147+
onChange: PropTypes.func.isRequired,
148+
forID: PropTypes.string,
149+
value: PropTypes.node,
150+
classNameWrapper: PropTypes.string.isRequired,
151+
};
152+
153+
Control.defaultProps = {
154+
value: '',
155+
};
156+
157+
export const Widget = {
158+
// name that will be used in config.yml
159+
name: 'cc-permalink',
160+
controlComponent: Control,
161+
};

package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "netlify-cms-widget-permalink",
3+
"version": "0.9.0",
4+
"description": "Permalink widget for Netlify CMS",
5+
"main": "./index.js",
6+
"keywords": [],
7+
"author": "Wojciech Kaluzny <wk@cleancommit.io>",
8+
"license": "MIT",
9+
"repository": "https://github.com/clean-commit/netlify-cms-widget-permalink",
10+
"peerDependencies": {
11+
"react": "^16.8.6",
12+
"react-dom": "^16.8.6"
13+
},
14+
"publishConfig": {
15+
"access": "public"
16+
},
17+
"devDependencies": {
18+
"@types/react": "^16.8.23"
19+
}
20+
}

readme.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Netlify CMS Permalink Widget
2+
3+
Netlify CMS Widget that allows you to create customizable permalinks easily.
4+
5+
Widgets are inputs for the Netlify CMS editor interface. It's a React component that receives user input and outputs a serialized value.
6+
7+
This widget allows you to create custom permalinks that can be used to generate pathnames for Netlify CMS based websites. The widget mimics the behaviour of WordPress permalink input field automatically outputting correct slugs.
8+
9+
![Permalink widget](ezgif-5-f5c945ae99.gif)
10+
11+
## Installation
12+
13+
1. Install the widget
14+
15+
```
16+
npm install netlify-cms-widget-permalink
17+
```
18+
19+
2. Import the Permalink widget to your Netlify CMS setup file
20+
21+
```
22+
import {PermalinkControl, PermalinkPreview} from 'netlify-cms-widget-permalink';
23+
```
24+
25+
3. Register the widget for use
26+
27+
```
28+
CMS.registerWidget('permalink', PermalinkControl, PermalinkPreview)
29+
```
30+
31+
## Usage details
32+
33+
Inside the YML collecitons file you can use `permalink` as new widget
34+
35+
```
36+
collections:
37+
- name: "example"
38+
label: "Example"
39+
folder: "/path/to/your/folder"
40+
create: true
41+
slug: "{{slug}}"
42+
fields:
43+
- {label: "ID", name: "id", widget: "permalink", prefix: 'blog', url: 'http://example.com'}
44+
```
45+
46+
You can also use it as a JS object using Netlify CMS [Manual Initialization](https://www.netlifycms.org/docs/beta-features/#manual-initialization)
47+
48+
Example:
49+
50+
```
51+
{
52+
label: 'Permalink',
53+
name: 'permalink',
54+
widget: 'permalink',
55+
required: true,
56+
url: 'http://localhost:8000',
57+
hint: 'The post URL (do not include folder or file extension)',
58+
},
59+
```
60+
61+
This widget is using 2 custom items:
62+
`url` -> is used to visualize and link to the page
63+
`prefix` -> is used when you want to prefix some pages (i.e. `blog` will output -> `/blog/your-slug/`)
64+
65+
### Additional guides
66+
67+
- [Efficient Netlify CMS config with Manual Initialization](https://mrkaluzny.com/blog/dry-netlify-cms-config-with-manual-initialization?utm_source=GitHub&utm_medium=henlo-gatsby)
68+
- [How to optimize SEO with Gatsby & Netlify CMS](https://mrkaluzny.com/blog/how-to-optimize-seo-with-gatsby-netlify?utm_source=GitHub&utm_medium=henlo-gatsby)
69+
- [Full text search with Gatsby & Netlify CMS](https://mrkaluzny.com/blog/full-text-search-with-gatsby-and-netlify-cms?utm_source=GitHub&utm_medium=henlo-gatsby)

0 commit comments

Comments
 (0)