strapi-plugin-sitemap
Advanced tools
Comparing version 2.0.0-beta.1 to 2.0.0-beta.2
@@ -6,2 +6,3 @@ import React from 'react'; | ||
import { useNotification } from '@strapi/helper-plugin'; | ||
import { HeaderLayout } from '@strapi/parts/Layout'; | ||
@@ -12,3 +13,3 @@ import { Box } from '@strapi/parts/Box'; | ||
import { submit } from '../../state/actions/Sitemap'; | ||
import { discardAllChanges, submit } from '../../state/actions/Sitemap'; | ||
@@ -18,2 +19,3 @@ const Header = () => { | ||
const initialData = useSelector((state) => state.getIn(['sitemap', 'initialData'], Map())); | ||
const toggleNotification = useNotification(); | ||
@@ -27,5 +29,10 @@ const dispatch = useDispatch(); | ||
e.preventDefault(); | ||
dispatch(submit(settings.toJS())); | ||
dispatch(submit(settings.toJS(), toggleNotification)); | ||
}; | ||
const handleCancel = (e) => { | ||
e.preventDefault(); | ||
dispatch(discardAllChanges()); | ||
}; | ||
return ( | ||
@@ -35,11 +42,23 @@ <Box background="neutral100"> | ||
primaryAction={( | ||
<Button | ||
onClick={handleSubmit} | ||
disabled={disabled} | ||
type="submit" | ||
startIcon={<CheckIcon />} | ||
size="L" | ||
> | ||
{formatMessage({ id: 'sitemap.Button.Save' })} | ||
</Button> | ||
<Box style={{ display: "flex" }}> | ||
<Button | ||
onClick={handleCancel} | ||
disabled={disabled} | ||
type="cancel" | ||
size="L" | ||
variant="secondary" | ||
> | ||
{formatMessage({ id: 'sitemap.Button.Cancel' })} | ||
</Button> | ||
<Button | ||
style={{ marginLeft: '10px' }} | ||
onClick={handleSubmit} | ||
disabled={disabled} | ||
type="submit" | ||
startIcon={<CheckIcon />} | ||
size="L" | ||
> | ||
{formatMessage({ id: 'sitemap.Button.Save' })} | ||
</Button> | ||
</Box> | ||
)} | ||
@@ -46,0 +65,0 @@ title={formatMessage({ id: 'sitemap.Header.Title' })} |
import React from 'react'; | ||
import { isEmpty } from 'lodash'; | ||
import { Map } from 'immutable'; | ||
@@ -8,29 +7,116 @@ import { useIntl } from 'react-intl'; | ||
import { Text } from '@strapi/parts/Text'; | ||
import { useNotification } from '@strapi/helper-plugin'; | ||
import { Text, H3 } from '@strapi/parts/Text'; | ||
import { Box } from '@strapi/parts/Box'; | ||
import { Button } from '@strapi/parts/Button'; | ||
import styled from 'styled-components'; | ||
import { Link } from '@strapi/parts/Link'; | ||
import { TextInput } from '@strapi/parts/TextInput'; | ||
import { generateSitemap } from '../../state/actions/Sitemap'; | ||
import { generateSitemap, onChangeSettings } from '../../state/actions/Sitemap'; | ||
import { formatTime } from '../../helpers/timeFormat'; | ||
const Info = () => { | ||
const settings = useSelector((state) => state.getIn(['sitemap', 'settings'], Map())); | ||
const sitemapPresence = useSelector((state) => state.getIn(['sitemap', 'sitemapPresence'], Map())); | ||
const hasHostname = useSelector((state) => state.getIn(['sitemap', 'initialData', 'hostname'], Map())); | ||
const sitemapInfo = useSelector((state) => state.getIn(['sitemap', 'info'], Map())); | ||
const dispatch = useDispatch(); | ||
const toggleNotification = useNotification(); | ||
const { formatMessage } = useIntl(); | ||
const settingsComplete = settings.get('hostname') && !isEmpty(settings.get('contentTypes')) | ||
|| settings.get('hostname') && !isEmpty(settings.get('customEntries')) | ||
|| settings.get('hostname') && settings.get('includeHomepage'); | ||
const updateDate = new Date(sitemapInfo.get('updateTime')); | ||
const { formatMessage } = useIntl(); | ||
// Format month, day and time. | ||
const month = updateDate.toLocaleString('en', { month: 'numeric' }); | ||
const day = updateDate.toLocaleString('en', { day: 'numeric' }); | ||
const year = updateDate.getFullYear().toString().substr(-2); | ||
const time = formatTime(updateDate, true); | ||
const StatusWrapper = styled(Box)` | ||
${Text} { | ||
color: ${({ theme, textColor }) => theme.colors[textColor]}; | ||
const content = () => { | ||
if (!hasHostname) { | ||
return ( | ||
<div> | ||
<H3 style={{ marginBottom: '10px' }}> | ||
Set your hostname | ||
</H3> | ||
<div> | ||
<Text> | ||
Before you can generate the sitemap you have to specify the hostname of your website. | ||
</Text> | ||
<Box paddingTop={4}> | ||
<TextInput | ||
placeholder="https://www.strapi.io" | ||
label={formatMessage({ id: 'sitemap.Settings.Field.Hostname.Label' })} | ||
name="hostname" | ||
value={settings.get('hostname')} | ||
onChange={(e) => dispatch(onChangeSettings('hostname', e.target.value))} | ||
/> | ||
</Box> | ||
</div> | ||
</div> | ||
); | ||
} else if (sitemapInfo.size === 0) { | ||
return ( | ||
<div> | ||
<H3 style={{ marginBottom: '10px' }}> | ||
No sitemap XML present | ||
</H3> | ||
<div> | ||
<Text> | ||
Generate your first sitemap XML with the button below. | ||
</Text> | ||
<Button | ||
onClick={() => dispatch(generateSitemap(toggleNotification))} | ||
variant="secondary" | ||
style={{ marginTop: '15px' }} | ||
> | ||
{formatMessage({ id: 'sitemap.Header.Button.Generate' })} | ||
</Button> | ||
</div> | ||
</div> | ||
); | ||
} else { | ||
return ( | ||
<div> | ||
<H3 style={{ marginBottom: '10px' }}> | ||
Sitemap XML is present | ||
</H3> | ||
<div> | ||
<Text> | ||
Last updated at: | ||
</Text> | ||
<Text bold style={{ marginLeft: '5px' }}> | ||
{`${month}/${day}/${year} - ${time}`} | ||
</Text> | ||
</div> | ||
<div style={{ marginBottom: '15px' }}> | ||
<Text> | ||
Amount of URLs: | ||
</Text> | ||
<Text bold style={{ marginLeft: '5px' }}> | ||
{sitemapInfo.get('urls')} | ||
</Text> | ||
</div> | ||
<div style={{ display: 'flex', flexDirection: 'row' }}> | ||
<Button | ||
onClick={() => dispatch(generateSitemap(toggleNotification))} | ||
variant="secondary" | ||
style={{ marginRight: '10px' }} | ||
> | ||
{formatMessage({ id: 'sitemap.Header.Button.Generate' })} | ||
</Button> | ||
<Link | ||
href={sitemapInfo.get('location')} | ||
target="_blank" | ||
> | ||
Go to the sitemap | ||
</Link> | ||
</div> | ||
</div> | ||
); | ||
} | ||
`; | ||
}; | ||
return ( | ||
<Box padding={8}> | ||
<StatusWrapper | ||
<Box paddingLeft={8} paddingRight={8}> | ||
<Box | ||
borderColor="secondary200" | ||
@@ -44,19 +130,4 @@ background="secondary100" | ||
> | ||
{sitemapPresence ? ( | ||
<Text> | ||
A sitemap has previously been generated, see the info below. | ||
</Text> | ||
) : ( | ||
<Text> | ||
You have yet to generate your first sitemap. Finish the settings below to do a one-time generate. | ||
</Text> | ||
)} | ||
<Button | ||
onClick={() => dispatch(generateSitemap())} | ||
variant="tertiary" | ||
disabled={!settingsComplete} | ||
> | ||
{formatMessage({ id: 'sitemap.Header.Button.Generate' })} | ||
</Button> | ||
</StatusWrapper> | ||
{content()} | ||
</Box> | ||
</Box> | ||
@@ -63,0 +134,0 @@ ); |
@@ -8,3 +8,2 @@ import React from 'react'; | ||
import { Select, Option } from '@strapi/parts/Select'; | ||
import { Tabs, Tab, TabGroup, TabPanels, TabPanel } from '@strapi/parts/Tabs'; | ||
@@ -14,2 +13,3 @@ import SelectContentTypes from '../../SelectContentTypes'; | ||
import form from '../mapper'; | ||
import SelectLanguage from '../../SelectLanguage'; | ||
@@ -28,2 +28,4 @@ const CollectionForm = (props) => { | ||
setUid, | ||
langcode, | ||
setLangcode, | ||
patternInvalid, | ||
@@ -33,11 +35,6 @@ setPatternInvalid, | ||
const handleSelectChange = (contentType) => { | ||
const handleSelectChange = (contentType, lang = 'und') => { | ||
setLangcode(lang); | ||
setUid(contentType); | ||
// Set initial values | ||
onCancel(false); | ||
Object.keys(form).map((input) => { | ||
onChange(contentType, input, form[input].value); | ||
}); | ||
onChange(contentType, 'excluded', []); | ||
}; | ||
@@ -48,3 +45,2 @@ | ||
let suffix = ''; | ||
console.log(allowedFields[uid]); | ||
if (allowedFields[uid]) { | ||
@@ -67,60 +63,62 @@ suffix = ' using '; | ||
return ( | ||
<TabGroup label="Some stuff for the label" id="tabs" variant="simple"> | ||
<Tabs style={{ display: 'flex', justifyContent: 'flex-end' }}> | ||
<Tab>Base settings</Tab> | ||
<Tab>Advanced settings</Tab> | ||
</Tabs> | ||
<form style={{ borderTop: '1px solid #f5f5f6', paddingTop: 30 }}> | ||
<Grid gap={6}> | ||
<GridItem col={6} s={12}> | ||
<SelectContentTypes | ||
contentTypes={contentTypes} | ||
onChange={(value) => handleSelectChange(value)} | ||
value={uid} | ||
disabled={id} | ||
modifiedContentTypes={modifiedState} | ||
/> | ||
</GridItem> | ||
<GridItem col={6} s={12}> | ||
<TabPanels> | ||
<TabPanel> | ||
<div> | ||
<TextInput | ||
label={formatMessage({ id: 'sitemap.Settings.Field.Pattern.Label' })} | ||
name="pattern" | ||
value={modifiedState.getIn([uid, 'pattern'], '')} | ||
hint={patternHint()} | ||
disabled={!uid} | ||
error={patternInvalid.invalid ? patternInvalid.message : ''} | ||
placeholder="/en/pages/[id]" | ||
onChange={async (e) => { | ||
if (e.target.value.match(/^[A-Za-z0-9-_.~[\]/]*$/)) { | ||
onChange(uid, 'pattern', e.target.value); | ||
setPatternInvalid({ invalid: false }); | ||
} | ||
}} | ||
/> | ||
</div> | ||
</TabPanel> | ||
<TabPanel> | ||
{Object.keys(form).map((input) => ( | ||
<Select | ||
name={input} | ||
key={input} | ||
{...form[input]} | ||
disabled={!uid} | ||
onChange={(value) => onChange(uid, input, value)} | ||
value={modifiedState.getIn([uid, input], form[input].value)} | ||
> | ||
{form[input].options.map((option) => ( | ||
<Option value={option} key={option}>{option}</Option> | ||
))} | ||
</Select> | ||
))} | ||
</TabPanel> | ||
</TabPanels> | ||
</GridItem> | ||
</Grid> | ||
</form> | ||
</TabGroup> | ||
<form style={{ borderTop: '1px solid #f5f5f6', paddingTop: 30 }}> | ||
<Grid gap={6}> | ||
<GridItem col={6} s={12}> | ||
<Grid gap={4}> | ||
<GridItem col={12}> | ||
<SelectContentTypes | ||
contentTypes={contentTypes} | ||
onChange={(value) => handleSelectChange(value)} | ||
value={uid} | ||
disabled={id} | ||
modifiedContentTypes={modifiedState} | ||
/> | ||
</GridItem> | ||
<GridItem col={12}> | ||
<SelectLanguage | ||
contentType={contentTypes[uid]} | ||
onChange={(value) => handleSelectChange(uid, value)} | ||
value={langcode} | ||
/> | ||
</GridItem> | ||
</Grid> | ||
</GridItem> | ||
<GridItem col={6} s={12}> | ||
<Grid gap={4}> | ||
<GridItem col={12}> | ||
<TextInput | ||
label={formatMessage({ id: 'sitemap.Settings.Field.Pattern.Label' })} | ||
name="pattern" | ||
value={modifiedState.getIn([uid, 'languages', langcode, 'pattern'], '')} | ||
hint={patternHint()} | ||
disabled={!uid || (contentTypes[uid].locales && langcode === 'und')} | ||
error={patternInvalid.invalid ? patternInvalid.message : ''} | ||
placeholder="/en/pages/[id]" | ||
onChange={async (e) => { | ||
if (e.target.value.match(/^[A-Za-z0-9-_.~[\]/]*$/)) { | ||
onChange(uid, langcode, 'pattern', e.target.value); | ||
setPatternInvalid({ invalid: false }); | ||
} | ||
}} | ||
/> | ||
</GridItem> | ||
{Object.keys(form).map((input) => ( | ||
<GridItem col={12} key={input}> | ||
<Select | ||
name={input} | ||
{...form[input]} | ||
disabled={!uid || (contentTypes[uid].locales && langcode === 'und')} | ||
onChange={(value) => onChange(uid, langcode, input, value)} | ||
value={modifiedState.getIn([uid, 'languages', langcode, input], form[input].value)} | ||
> | ||
{form[input].options.map((option) => ( | ||
<Option value={option} key={option}>{option}</Option> | ||
))} | ||
</Select> | ||
</GridItem> | ||
))} | ||
</Grid> | ||
</GridItem> | ||
</Grid> | ||
</form> | ||
); | ||
@@ -127,0 +125,0 @@ }; |
@@ -53,16 +53,19 @@ import React from 'react'; | ||
<GridItem col={6} s={12}> | ||
{Object.keys(form).map((input) => ( | ||
<Select | ||
name={input} | ||
key={input} | ||
{...form[input]} | ||
disabled={!uid} | ||
onChange={(value) => onChange(uid, input, value)} | ||
value={modifiedState.getIn([uid, input], form[input].value)} | ||
> | ||
{form[input].options.map((option) => ( | ||
<Option value={option} key={option}>{option}</Option> | ||
))} | ||
</Select> | ||
))} | ||
<Grid gap={4}> | ||
{Object.keys(form).map((input) => ( | ||
<GridItem col={12} key={input}> | ||
<Select | ||
name={input} | ||
{...form[input]} | ||
disabled={!uid} | ||
onChange={(value) => onChange(uid, input, value)} | ||
value={modifiedState.getIn([uid, input], form[input].value)} | ||
> | ||
{form[input].options.map((option) => ( | ||
<Option value={option} key={option}>{option}</Option> | ||
))} | ||
</Select> | ||
</GridItem> | ||
))} | ||
</Grid> | ||
</GridItem> | ||
@@ -69,0 +72,0 @@ </Grid> |
@@ -14,2 +14,3 @@ import React, { useState, useEffect } from 'react'; | ||
const [uid, setUid] = useState(''); | ||
const [langcode, setLangcode] = useState('und'); | ||
const [patternInvalid, setPatternInvalid] = useState({ invalid: false }); | ||
@@ -23,4 +24,6 @@ const { formatMessage } = useIntl(); | ||
id, | ||
lang, | ||
type, | ||
modifiedState, | ||
contentTypes, | ||
} = props; | ||
@@ -36,2 +39,8 @@ | ||
} | ||
if (lang && langcode === 'und') { | ||
setLangcode(lang); | ||
} else { | ||
setLangcode('und'); | ||
} | ||
}, [isOpen]); | ||
@@ -48,3 +57,3 @@ | ||
body: { | ||
pattern: modifiedState.getIn([uid, 'pattern'], null), | ||
pattern: modifiedState.getIn([uid, 'languages', langcode, 'pattern'], null), | ||
modelName: uid, | ||
@@ -63,3 +72,3 @@ }, | ||
case 'collection': | ||
return <CollectionForm uid={uid} setUid={setUid} setPatternInvalid={setPatternInvalid} patternInvalid={patternInvalid} {...props} />; | ||
return <CollectionForm uid={uid} setUid={setUid} langcode={langcode} setLangcode={setLangcode} setPatternInvalid={setPatternInvalid} patternInvalid={patternInvalid} {...props} />; | ||
case 'custom': | ||
@@ -94,3 +103,3 @@ return <CustomForm uid={uid} setUid={setUid} {...props} />; | ||
onClick={submitForm} | ||
disabled={!uid} | ||
disabled={!uid || (contentTypes && contentTypes[uid].locales && langcode === 'und')} | ||
> | ||
@@ -97,0 +106,0 @@ {formatMessage({ id: 'sitemap.Button.Save' })} |
@@ -5,3 +5,2 @@ import React from 'react'; | ||
const SelectContentTypes = (props) => { | ||
const { | ||
@@ -12,20 +11,4 @@ contentTypes, | ||
value, | ||
modifiedContentTypes, | ||
} = props; | ||
const filterOptions = (options) => { | ||
const newOptions = {}; | ||
// Remove the contentypes which are allready set in the sitemap. | ||
Object.entries(options).map(([i, e]) => { | ||
if (!modifiedContentTypes.get(i) || value === i) { | ||
newOptions[i] = e; | ||
} | ||
}); | ||
return newOptions; | ||
}; | ||
const options = filterOptions(contentTypes); | ||
return ( | ||
@@ -40,3 +23,3 @@ <Select | ||
> | ||
{Object.entries(options).map(([uid, { displayName }]) => { | ||
{Object.entries(contentTypes).map(([uid, { displayName }]) => { | ||
return <Option value={uid} key={uid}>{displayName}</Option>; | ||
@@ -46,19 +29,4 @@ })} | ||
); | ||
// return ( | ||
// <> | ||
// <Label htmlFor="select" message="Content Type" /> | ||
// <Select | ||
// name="select" | ||
// label="test" | ||
// onChange={(e) => onChange(e)} | ||
// options={Object.keys(options)} | ||
// value={value} | ||
// disabled={disabled} | ||
// /> | ||
// <p style={{ color: '#9ea7b8', fontSize: 12, marginTop: 5, marginBottom: 20 }}>Select a content type.</p> | ||
// </> | ||
// ); | ||
}; | ||
export default SelectContentTypes; |
@@ -19,3 +19,3 @@ import React from 'react'; | ||
<TabPanel> | ||
<Box padding={4} background="neutral0"> | ||
<Box padding={6} background="neutral0"> | ||
<CollectionURLs /> | ||
@@ -25,3 +25,3 @@ </Box> | ||
<TabPanel> | ||
<Box padding={4} background="neutral0"> | ||
<Box padding={6} background="neutral0"> | ||
<CustomURLs /> | ||
@@ -31,3 +31,3 @@ </Box> | ||
<TabPanel> | ||
<Box padding={4} background="neutral0"> | ||
<Box padding={6} background="neutral0"> | ||
<Settings /> | ||
@@ -34,0 +34,0 @@ </Box> |
@@ -25,4 +25,4 @@ /** | ||
export const HAS_SITEMAP = 'Sitemap/ConfigPage/HAS_SITEMAP'; | ||
export const HAS_SITEMAP_SUCCEEDED = 'Sitemap/ConfigPage/HAS_SITEMAP_SUCCEEDED'; | ||
export const GET_SITEMAP_INFO_SUCCEEDED = 'Sitemap/ConfigPage/GET_SITEMAP_INFO_SUCCEEDED'; | ||
export const ON_CHANGE_CUSTOM_ENTRY = 'Sitemap/ConfigPage/ON_CHANGE_CUSTOM_ENTRY'; | ||
export const GET_ALLOWED_FIELDS_SUCCEEDED = 'Sitemap/ConfigPage/GET_ALLOWED_FIELDS_SUCCEEDED'; |
@@ -11,2 +11,4 @@ /** | ||
import { useNotification } from '@strapi/helper-plugin'; | ||
import Tabs from '../../components/Tabs'; | ||
@@ -16,12 +18,13 @@ import Header from '../../components/Header'; | ||
import { getAllowedFields, getContentTypes, getSettings, hasSitemap } from '../../state/actions/Sitemap'; | ||
import { getAllowedFields, getContentTypes, getSettings, getSitemapInfo } from '../../state/actions/Sitemap'; | ||
const App = () => { | ||
const dispatch = useDispatch(); | ||
const toggleNotification = useNotification(); | ||
useEffect(() => { | ||
dispatch(getSettings()); | ||
dispatch(getContentTypes()); | ||
dispatch(hasSitemap()); | ||
dispatch(getAllowedFields()); | ||
dispatch(getSettings(toggleNotification)); | ||
dispatch(getContentTypes(toggleNotification)); | ||
dispatch(getSitemapInfo(toggleNotification)); | ||
dispatch(getAllowedFields(toggleNotification)); | ||
}, [dispatch]); | ||
@@ -28,0 +31,0 @@ |
@@ -23,3 +23,3 @@ /** | ||
UPDATE_SETTINGS, | ||
HAS_SITEMAP_SUCCEEDED, | ||
GET_SITEMAP_INFO_SUCCEEDED, | ||
ON_CHANGE_CUSTOM_ENTRY, | ||
@@ -32,3 +32,3 @@ GET_ALLOWED_FIELDS_SUCCEEDED, | ||
// Get initial settings | ||
export function getSettings() { | ||
export function getSettings(toggleNotification) { | ||
return async function(dispatch) { | ||
@@ -39,3 +39,3 @@ try { | ||
} catch (err) { | ||
strapi.notification.toggle({ type: 'warning', message: { id: 'notification.error' } }); | ||
toggleNotification({ type: 'warning', message: { id: 'notification.error' } }); | ||
} | ||
@@ -52,6 +52,7 @@ }; | ||
export function onChangeContentTypes(contentType, key, value) { | ||
export function onChangeContentTypes(contentType, lang, key, value) { | ||
return { | ||
type: ON_CHANGE_CONTENT_TYPES, | ||
contentType, | ||
lang, | ||
key, | ||
@@ -98,10 +99,10 @@ value, | ||
export function generateSitemap() { | ||
export function generateSitemap(toggleNotification) { | ||
return async function(dispatch) { | ||
try { | ||
const { message } = await request('/sitemap', { method: 'GET' }); | ||
dispatch(hasSitemap()); | ||
strapi.notification.toggle({ type: 'success', message }); | ||
dispatch(getSitemapInfo()); | ||
toggleNotification({ type: 'success', message }); | ||
} catch (err) { | ||
strapi.notification.toggle({ type: 'warning', message: { id: 'notification.error' } }); | ||
toggleNotification({ type: 'warning', message: { id: 'notification.error' } }); | ||
} | ||
@@ -111,3 +112,3 @@ }; | ||
export function getContentTypes() { | ||
export function getContentTypes(toggleNotification) { | ||
return async function(dispatch) { | ||
@@ -118,3 +119,3 @@ try { | ||
} catch (err) { | ||
strapi.notification.toggle({ type: 'warning', message: { id: 'notification.error' } }); | ||
toggleNotification({ type: 'warning', message: { id: 'notification.error' } }); | ||
} | ||
@@ -131,3 +132,3 @@ }; | ||
export function submit(settings) { | ||
export function submit(settings, toggleNotification) { | ||
return async function(dispatch) { | ||
@@ -137,5 +138,5 @@ try { | ||
dispatch(onSubmitSucceeded()); | ||
strapi.notification.toggle({ type: 'success', message: { id: getTrad('notification.success.submit') } }); | ||
toggleNotification({ type: 'success', message: { id: getTrad('notification.success.submit') } }); | ||
} catch (err) { | ||
strapi.notification.toggle({ type: 'warning', message: { id: 'notification.error' } }); | ||
toggleNotification({ type: 'warning', message: { id: 'notification.error' } }); | ||
} | ||
@@ -157,6 +158,7 @@ }; | ||
export function deleteContentType(key) { | ||
export function deleteContentType(key, lang) { | ||
return { | ||
type: DELETE_CONTENT_TYPE, | ||
key, | ||
lang, | ||
}; | ||
@@ -172,9 +174,9 @@ } | ||
export function hasSitemap() { | ||
export function getSitemapInfo(toggleNotification) { | ||
return async function(dispatch) { | ||
try { | ||
const { main } = await request('/sitemap/presence', { method: 'GET' }); | ||
dispatch(hasSitemapSucceeded(main)); | ||
const info = await request('/sitemap/info', { method: 'GET' }); | ||
dispatch(getSitemapInfoSucceeded(info)); | ||
} catch (err) { | ||
strapi.notification.toggle({ type: 'warning', message: { id: 'notification.error' } }); | ||
toggleNotification({ type: 'warning', message: { id: 'notification.error' } }); | ||
} | ||
@@ -184,10 +186,10 @@ }; | ||
export function hasSitemapSucceeded(main) { | ||
export function getSitemapInfoSucceeded(info) { | ||
return { | ||
type: HAS_SITEMAP_SUCCEEDED, | ||
hasSitemap: main, | ||
type: GET_SITEMAP_INFO_SUCCEEDED, | ||
info, | ||
}; | ||
} | ||
export function getAllowedFields() { | ||
export function getAllowedFields(toggleNotification) { | ||
return async function(dispatch) { | ||
@@ -198,3 +200,3 @@ try { | ||
} catch (err) { | ||
strapi.notification.toggle({ type: 'warning', message: { id: 'notification.error' } }); | ||
toggleNotification({ type: 'warning', message: { id: 'notification.error' } }); | ||
} | ||
@@ -201,0 +203,0 @@ }; |
@@ -21,3 +21,3 @@ /** | ||
UPDATE_SETTINGS, | ||
HAS_SITEMAP_SUCCEEDED, | ||
GET_SITEMAP_INFO_SUCCEEDED, | ||
ON_CHANGE_CUSTOM_ENTRY, | ||
@@ -28,3 +28,3 @@ GET_ALLOWED_FIELDS_SUCCEEDED, | ||
const initialState = fromJS({ | ||
sitemapPresence: false, | ||
info: {}, | ||
allowedFields: {}, | ||
@@ -55,4 +55,9 @@ settings: Map({}), | ||
case ON_CHANGE_CONTENT_TYPES: | ||
return state | ||
.updateIn(['modifiedContentTypes', action.contentType, action.key], () => action.value); | ||
if (action.lang) { | ||
return state | ||
.updateIn(['modifiedContentTypes', action.contentType, 'languages', action.lang, action.key], () => action.value); | ||
} else { | ||
return state | ||
.updateIn(['modifiedContentTypes', action.contentType, action.key], () => action.value); | ||
} | ||
case ON_CHANGE_CUSTOM_ENTRY: | ||
@@ -78,5 +83,11 @@ return state | ||
case DELETE_CONTENT_TYPE: | ||
return state | ||
.deleteIn(['settings', 'contentTypes', action.key]) | ||
.deleteIn(['modifiedContentTypes', action.key]); | ||
if (state.getIn(['settings', 'contentTypes', action.key, 'languages']).size > 1) { | ||
return state | ||
.deleteIn(['settings', 'contentTypes', action.key, 'languages', action.lang]) | ||
.deleteIn(['modifiedContentTypes', action.key, 'languages', action.lang]); | ||
} else { | ||
return state | ||
.deleteIn(['settings', 'contentTypes', action.key]) | ||
.deleteIn(['modifiedContentTypes', action.key]); | ||
} | ||
case DELETE_CUSTOM_ENTRY: | ||
@@ -92,5 +103,5 @@ return state | ||
.update('initialData', () => state.get('settings')); | ||
case HAS_SITEMAP_SUCCEEDED: | ||
case GET_SITEMAP_INFO_SUCCEEDED: | ||
return state | ||
.update('sitemapPresence', () => action.hasSitemap); | ||
.update('info', () => fromJS(action.info)); | ||
case GET_ALLOWED_FIELDS_SUCCEEDED: | ||
@@ -97,0 +108,0 @@ return state |
@@ -6,3 +6,3 @@ import React, { useState } from 'react'; | ||
import { deleteContentType, discardModifiedContentTypes, onChangeContentTypes, submitModal } from '../../state/actions/Sitemap'; | ||
import List from '../../components/List'; | ||
import List from '../../components/List/Collection'; | ||
import ModalForm from '../../components/ModalForm'; | ||
@@ -15,2 +15,3 @@ | ||
const [uid, setUid] = useState(null); | ||
const [langcode, setLangcode] = useState('und'); | ||
@@ -24,4 +25,5 @@ const handleModalSubmit = (e) => { | ||
const handleModalOpen = (editId) => { | ||
const handleModalOpen = (editId, lang) => { | ||
if (editId) setUid(editId); | ||
if (lang) setLangcode(lang); | ||
setModalOpen(true); | ||
@@ -31,5 +33,7 @@ }; | ||
const handleModalClose = (closeModal = true) => { | ||
if (closeModal) setModalOpen(false); | ||
if (closeModal) { | ||
setModalOpen(false); | ||
setUid(null); | ||
} | ||
dispatch(discardModifiedContentTypes()); | ||
setUid(null); | ||
}; | ||
@@ -46,4 +50,4 @@ | ||
items={state.getIn(['settings', 'contentTypes'])} | ||
openModal={(editId) => handleModalOpen(editId)} | ||
onDelete={(key) => dispatch(deleteContentType(key))} | ||
openModal={(editId, lang) => handleModalOpen(editId, lang)} | ||
onDelete={(key, lang) => dispatch(deleteContentType(key, lang))} | ||
/> | ||
@@ -56,5 +60,6 @@ <ModalForm | ||
onCancel={(closeModal) => handleModalClose(closeModal)} | ||
onChange={(contentType, key, value) => dispatch(onChangeContentTypes(contentType, key, value))} | ||
onChange={(contentType, lang, key, value) => dispatch(onChangeContentTypes(contentType, lang, key, value))} | ||
isOpen={modalOpen} | ||
id={uid} | ||
lang={langcode} | ||
type="collection" | ||
@@ -61,0 +66,0 @@ /> |
@@ -6,3 +6,3 @@ import React, { useState } from 'react'; | ||
import { discardModifiedContentTypes, onChangeCustomEntry, submitModal, deleteCustomEntry } from '../../state/actions/Sitemap'; | ||
import List from '../../components/List'; | ||
import List from '../../components/List/Custom'; | ||
import ModalForm from '../../components/ModalForm'; | ||
@@ -9,0 +9,0 @@ |
@@ -18,3 +18,3 @@ import React from 'react'; | ||
return ( | ||
<Grid gap={6}> | ||
<Grid gap={4}> | ||
<GridItem col={6} s={12}> | ||
@@ -52,13 +52,2 @@ <TextInput | ||
</GridItem> | ||
<GridItem col={12} s={12}> | ||
<ToggleInput | ||
hint={formatMessage({ id: 'sitemap.Settings.Field.AutoGenerate.Description' })} | ||
label={formatMessage({ id: 'sitemap.Settings.Field.AutoGenerate.Label' })} | ||
name="autoGenerate" | ||
onLabel="on" | ||
offLabel="off" | ||
checked={settings.get('autoGenerate')} | ||
onChange={(e) => dispatch(onChangeSettings('autoGenerate', e.target.checked))} | ||
/> | ||
</GridItem> | ||
</Grid> | ||
@@ -65,0 +54,0 @@ ); |
{ | ||
"name": "strapi-plugin-sitemap", | ||
"version": "2.0.0-beta.1", | ||
"description": "A plugin for Strapi to create a customizable sitemap.", | ||
"version": "2.0.0-beta.2", | ||
"description": "Create a highly customizable sitemap XML in Strapi CMS.", | ||
"strapi": { | ||
@@ -20,3 +20,4 @@ "displayName": "Sitemap", | ||
"redux-thunk": "^2.3.0", | ||
"sitemap": "boazpoolman/sitemap.js#build" | ||
"sitemap": "boazpoolman/sitemap.js#build", | ||
"xml2js": "^0.4.23" | ||
}, | ||
@@ -46,2 +47,3 @@ "author": { | ||
"@strapi/parts": "^0.0.1-alpha.42", | ||
"@strapi/utils": "4.0.0-beta.2", | ||
"babel-eslint": "9.0.0", | ||
@@ -48,0 +50,0 @@ "codecov": "^3.8.3", |
209
README.md
@@ -1,3 +0,6 @@ | ||
# Strapi Plugin Sitemap | ||
<div align="center"> | ||
<h1>Strapi sitemap plugin</h1> | ||
<p style="margin-top: 0;">Create a highly customizable sitemap XML in Strapi CMS.</p> | ||
<p> | ||
@@ -17,44 +20,202 @@ <a href="https://www.npmjs.org/package/strapi-plugin-sitemap"> | ||
</p> | ||
</div> | ||
## ✨ Features | ||
This plugin is an integration of the UID field type. In Strapi you can manage your URLs by adding UID fields to your single or collection types. This field will act as a wrapper for the title field and will generate a unique SEO friendly path for each instance of the type. This plugin will then use those paths to generate a fully customizable sitemap for all your URLs. | ||
- **Multilingual** (Implements `rel="alternate"` for the translations of a page) | ||
- **Auto-updating** (Uses lifecycle methods to keep the sitemap XML up-to-date) | ||
- **URL bundles** (Bundle URLs by type and add them to the sitemap XML) | ||
- **Dynamic paths** (Implements URL patterns in which you can inject dynamic fields) | ||
- **Custom URLs** (URLs of pages which are not managed in Strapi) | ||
- **Styled with XSL** (Human readable XML styling) | ||
## Installation | ||
## ⏳ Installation | ||
Use `npm` or `yarn` to install and build the plugin. | ||
Install the plugin in your Strapi project. | ||
yarn add strapi-plugin-sitemap | ||
yarn build | ||
yarn develop | ||
```bash | ||
# using yarn | ||
yarn add strapi-plugin-sitemap | ||
## Configuration | ||
# using npm | ||
npm install strapi-plugin-sitemap --save | ||
``` | ||
Before you can generate the sitemap you need to specify what you want to be in it. In the admin section of the plugin you can add 'Collection entries' and 'Custom entries' to the sitemap. With collection entries you can add all URLs of a collection or single type, with custom entries you can add URLs which are not managed by Strapi. Also make sure to set the `hostname` of your website. | ||
After successful installation you have to rebuild the admin UI so it'll include this plugin. To rebuild and restart Strapi run: | ||
After saving the settings and generating the sitemap, it will be written in the `/public` folder of your Strapi project, making it available at `http://localhost:1337/sitemap.xml`. | ||
```bash | ||
# using yarn | ||
yarn build --clean | ||
yarn develop | ||
## Optional (but recommended) | ||
# using npm | ||
npm run build --clean | ||
npm run develop | ||
``` | ||
1. Add the `sitemap.xml` to the `.gitignore` of your project. | ||
The **Sitemap** plugin should appear in the **Plugins** section of Strapi sidebar after you run app again. | ||
2. Make sure the sitemap is always up-to-date. You can either add a cron job, or create a lifecycle method to run the `createSitemap()` service. | ||
Enjoy 🎉 | ||
## Cron job example | ||
## 🖐 Requirements | ||
// Generate the sitemap every 12 hours | ||
'0 */12 * * *': () => { | ||
strapi.plugins.sitemap.services.sitemap.createSitemap(); | ||
}, | ||
Complete installation requirements are the exact same as for Strapi itself and can be found in the [Strapi documentation](https://strapi.io/documentation). | ||
## Resources | ||
**Supported Strapi versions**: | ||
- [MIT License](LICENSE.md) | ||
- Strapi v4.0.0-beta.2 (recently tested) | ||
- Strapi v4.x | ||
- Strapi v3.6.x (use `strapi-plugin-sitemap@1.2.5`) | ||
## Links | ||
(This plugin may work with older Strapi versions, but these are not tested nor officially supported at this time.) | ||
**We recommend always using the latest version of Strapi to start your new projects**. | ||
## 💡 Usage | ||
With this plugin you have full control over which URLs you add to your sitemap XML. Go to the admin section of the plugin and start adding URLs. Here you will find that there are two ways to add URLs to the sitemap. With **URL bundles** and **Custom URLs**. | ||
### URL bundles | ||
A URL bundle is a set of URLs grouped by type. When adding a URL bundle to the sitemap you can define a **URL pattern** which will be used to generate all URLs in this bundle. (Read more about URL patterns below) | ||
URLs coming from a URL bundle will get the following XML attributes: | ||
- `<loc>` | ||
- `<lastmod>` | ||
- `<priority>` | ||
- `<changefreq>` | ||
### Custom URLs | ||
A custom URL is meant to add URLs to the sitemap which are not managed in Strapi. It might be that you have custom route like `/account` that is hardcoded in your front-end. If you'd want to add such a route (URL) to the sitemap you can add it as a custom URL. | ||
Custom URLs will get the following XML attributes: | ||
- `<loc>` | ||
- `<priority>` | ||
- `<changefreq>` | ||
## 🔌 URL pattern | ||
To create dynamic URLs this plugin uses **URL patterns**. A URL pattern is used when adding URL bundles to the sitemap and has the following format: | ||
``` | ||
/pages/[my-uid-field] | ||
``` | ||
Fields can be injected in the pattern by escaping them with `[]`. | ||
The following fields types are by default allowed in a pattern: | ||
- id | ||
- uid | ||
*Allowed field types can be altered with the `allowedFields` config. Read more about it below.* | ||
## 🌍 Multilingual | ||
When adding a URL bundle of a type which has localizations enabled you will be presented with a language dropdown in the settings form. You can now set a different URL pattern for each language. | ||
For each localization of a page the `<url>` in the sitemap XML will get an extra attribute: | ||
- `<xhtml:link rel="alternate">` | ||
This implementation is based on [Google's guidelines](https://developers.google.com/search/docs/advanced/crawling/localized-versions) on localized sitemaps. | ||
## ⚙️ Settings | ||
Settings can be changed in the admin section of the plugin. In the last tab (Settings) you will find the settings as described below. | ||
### Hostname (required) | ||
The hostname is the URL of your website. It will be used as the base URL of all URLs added to the sitemap XML. It is required to generate the XML file. | ||
###### Key: `hostname` | ||
> `required:` YES | `type:` string | `default:` '' | ||
### Exclude drafts | ||
When using the draft/publish functionality in Strapi this setting will make sure that all draft pages are excluded from the sitemap. If you want to have the draft pages in the sitemap anyways you can disable this setting. | ||
###### Key: `excludeDrafts` | ||
> `required:` NO | `type:` bool | `default:` true | ||
### Include homepage | ||
This setting will add a default `/` entry to the sitemap XML when none is present. The `/` entry corresponds to the homepage of your website. | ||
###### Key: `includeHomepage` | ||
> `required:` NO | `type:` bool | `default:` true | ||
## 🔧 Config | ||
Config can be changed in the `config/plugins.js` file in your Strapi project. | ||
You can overwrite the config like so: | ||
``` | ||
module.exports = ({ env }) => ({ | ||
'sitemap': { | ||
enabled: true, | ||
config: { | ||
autoGenerate: true, | ||
allowedFields: ['id', 'uid'], | ||
excludedTypes: [], | ||
}, | ||
}, | ||
}); | ||
``` | ||
### Auto generate | ||
When adding URL bundles to your sitemap XML, and auto generate is set to true, the plugin will utilize the lifecycle methods to regenerate the sitemap on `create`, `update` and `delete` for pages of the URL bundles type. This way your sitemap will always be up-to-date when making content changes. | ||
You might want to disable this setting if your experiencing performance issues. You could alternatively create a cronjob in which you generate the sitemap XML periodically. Like so: | ||
``` | ||
// Generate the sitemap every 12 hours | ||
'0 */12 * * *': () => { | ||
strapi.plugin('sitemap').service('sitemap').createSitemap(); | ||
}, | ||
``` | ||
###### Key: `autoGenerate ` | ||
> `required:` NO | `type:` bool | `default:` true | ||
### Allowed fields | ||
When defining a URL pattern you can populate it with dynamic fields. The fields allowed in the pattern are specified by type. By default only the field types `id` and `uid` are allowed in the pattern, but you can alter this setting to allow more field types in the pattern. | ||
*If you are missing a key field type of which you think it should be allowed by default please create an issue and explain why it is needed.* | ||
###### Key: `allowedFields ` | ||
> `required:` NO | `type:` array | `default:` `['id', 'uid']` | ||
### Excluded types | ||
This setting is just here for mere convenience. When adding a URL bundle to the sitemap you can specify the type for the bundle. This will show all types in Strapi, however some types should never be it's own page in a website and are therefor excluded in this setting. | ||
All types in this array will not be shown as an option when selecting the type of a URL bundle. | ||
###### Key: `excludedTypes ` | ||
> `required:` NO | `type:` array | `default:` `['admin::permission', 'admin::role', 'admin::api-token', 'plugin::i18n.locale', 'plugin::users-permissions.permission', 'plugin::users-permissions.role']` | ||
## 🤝 Contributing | ||
Feel free to fork and make a pull request of this plugin. All the input is welcome! | ||
## ⭐️ Show your support | ||
Give a star if this project helped you. | ||
## 🔗 Links | ||
- [NPM package](https://www.npmjs.com/package/strapi-plugin-sitemap) | ||
- [GitHub repository](https://github.com/boazpoolman/strapi-plugin-sitemap) | ||
## ⭐️ Show your support | ||
## 🌎 Community support | ||
Give a star if this project helped you. | ||
- For general help using Strapi, please refer to [the official Strapi documentation](https://strapi.io/documentation/). | ||
- You can contact me on the Strapi Discord [channel](https://discord.strapi.io/). | ||
## 📝 Resources | ||
- [MIT License](LICENSE.md) |
'use strict'; | ||
const fs = require('fs'); | ||
const { logMessage } = require('./utils'); | ||
const copyDir = require('./utils/copyDir'); | ||
@@ -24,4 +25,4 @@ | ||
} catch (error) { | ||
strapi.log.error(`Sitemap plugin bootstrap failed with error "${error.message}".`); | ||
strapi.log.error(logMessage(`Bootstrap failed with error "${error.message}".`)); | ||
} | ||
}; |
'use strict'; | ||
const sitemap = require('./sitemap'); | ||
const core = require('./core'); | ||
const pattern = require('./pattern'); | ||
const settings = require('./settings'); | ||
module.exports = { | ||
sitemap, | ||
core, | ||
pattern, | ||
settings, | ||
}; |
@@ -9,3 +9,3 @@ 'use strict'; | ||
path: "/", | ||
handler: "sitemap.buildSitemap", | ||
handler: "core.buildSitemap", | ||
config: { | ||
@@ -17,4 +17,4 @@ policies: [], | ||
method: "GET", | ||
path: "/presence", | ||
handler: "sitemap.hasSitemap", | ||
path: "/info", | ||
handler: "core.info", | ||
config: { | ||
@@ -26,4 +26,12 @@ policies: [], | ||
method: "GET", | ||
path: "/content-types", | ||
handler: "core.getContentTypes", | ||
config: { | ||
policies: [], | ||
}, | ||
}, | ||
{ | ||
method: "GET", | ||
path: "/settings", | ||
handler: "sitemap.getSettings", | ||
handler: "settings.getSettings", | ||
config: { | ||
@@ -36,3 +44,3 @@ policies: [], | ||
path: "/settings", | ||
handler: "sitemap.updateSettings", | ||
handler: "settings.updateSettings", | ||
config: { | ||
@@ -45,3 +53,3 @@ policies: [], | ||
path: "/settings/exclude", | ||
handler: "sitemap.excludeEntry", | ||
handler: "settings.excludeEntry", | ||
config: { | ||
@@ -54,3 +62,3 @@ policies: [], | ||
path: "/pattern/allowed-fields", | ||
handler: "sitemap.allowedFields", | ||
handler: "pattern.allowedFields", | ||
config: { | ||
@@ -63,3 +71,3 @@ policies: [], | ||
path: "/pattern/validate-pattern", | ||
handler: "sitemap.validatePattern", | ||
handler: "pattern.validatePattern", | ||
config: { | ||
@@ -69,11 +77,3 @@ policies: [], | ||
}, | ||
{ | ||
method: "GET", | ||
path: "/content-types", | ||
handler: "sitemap.getContentTypes", | ||
config: { | ||
policies: [], | ||
}, | ||
}, | ||
], | ||
}; |
'use strict'; | ||
const config = require('./config'); | ||
const core = require('./core'); | ||
const settings = require('./settings'); | ||
const pattern = require('./pattern'); | ||
const sitemap = require('./sitemap'); | ||
const lifecycle = require('./lifecycle'); | ||
module.exports = { | ||
config, | ||
sitemap, | ||
core, | ||
settings, | ||
pattern, | ||
lifecycle, | ||
}; |
'use strict'; | ||
const { getService } = require('../utils'); | ||
const { getService, logMessage } = require('../utils'); | ||
@@ -12,8 +12,8 @@ /** | ||
async loadLifecycleMethods() { | ||
const config = await getService('config').getConfig(); | ||
const sitemapService = await getService('sitemap'); | ||
const settings = await getService('settings').getConfig(); | ||
const sitemapService = await getService('core'); | ||
// Loop over configured contentTypes from store. | ||
if (config.contentTypes && config.autoGenerate) { | ||
Object.keys(config.contentTypes).map(async (contentType) => { | ||
if (settings.contentTypes && strapi.config.get('plugin.sitemap.autoGenerate')) { | ||
Object.keys(settings.contentTypes).map(async (contentType) => { | ||
if (strapi.contentTypes[contentType]) { | ||
@@ -48,3 +48,3 @@ await strapi.db.lifecycles.subscribe({ | ||
} else { | ||
strapi.log.error(`Sitemap plugin bootstrap failed. Could not load lifecycles on model '${contentType}'`); | ||
strapi.log.error(logMessage(`Bootstrap failed. Could not load lifecycles on model '${contentType}'`)); | ||
} | ||
@@ -51,0 +51,0 @@ }); |
@@ -7,4 +7,2 @@ 'use strict'; | ||
const allowedFields = ['id', 'uid']; | ||
/** | ||
@@ -19,3 +17,3 @@ * Get all field names allowed in the URL of a given content type. | ||
const fields = []; | ||
allowedFields.map((fieldType) => { | ||
strapi.config.get('plugin.sitemap.allowedFields').map((fieldType) => { | ||
Object.entries(contentType.attributes).map(([fieldName, field]) => { | ||
@@ -29,3 +27,5 @@ if (field.type === fieldType) { | ||
// Add id field manually because it is not on the attributes object of a content type. | ||
fields.push('id'); | ||
if (strapi.config.get('plugin.sitemap.allowedFields').includes('id')) { | ||
fields.push('id'); | ||
} | ||
@@ -32,0 +32,0 @@ return fields; |
@@ -7,3 +7,2 @@ 'use strict'; | ||
// retrieve a local service | ||
const getService = (name) => { | ||
@@ -13,5 +12,8 @@ return strapi.plugin('sitemap').service(name); | ||
const logMessage = (msg = '') => `[strapi-plugin-sitemap]: ${msg}`; | ||
module.exports = { | ||
getService, | ||
getCoreStore, | ||
logMessage, | ||
}; |
@@ -6,2 +6,3 @@ 'use strict'; | ||
const routes = require('./server/routes'); | ||
const config = require('./server/config'); | ||
const controllers = require('./server/controllers'); | ||
@@ -13,2 +14,3 @@ | ||
routes, | ||
config, | ||
controllers, | ||
@@ -15,0 +17,0 @@ services, |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
94867
74
2413
221
3
28
+ Addedxml2js@^0.4.23
+ Addedsax@1.4.1(transitive)
+ Addedxml2js@0.4.23(transitive)
+ Addedxmlbuilder@11.0.1(transitive)