Improved types and updated documentation (#71)

* v1.0.0-alpha44
This commit is contained in:
Daniel Lautzenheiser 2022-11-07 10:27:58 -05:00 committed by GitHub
parent ba1cde4e01
commit c55d1f912f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 3695 additions and 2546 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"name": "@staticcms/core",
"version": "1.0.0-alpha38",
"version": "1.0.0-alpha44",
"license": "MIT",
"description": "Static CMS core application.",
"repository": "https://github.com/StaticJsCMS/static-cms",

View File

@ -5,7 +5,6 @@ import trimStart from 'lodash/trimStart';
import yaml from 'yaml';
import { resolveBackend } from '../backend';
import { FILES, FOLDER } from '../constants/collectionTypes';
import { validateConfig } from '../constants/configSchema';
import { I18N, I18N_FIELD, I18N_STRUCTURE } from '../lib/i18n';
import { selectDefaultSortableFields } from '../lib/util/collection.util';
@ -22,6 +21,7 @@ import type {
ListField,
LocalBackend,
ObjectField,
UnknownField,
} from '../interface';
import type { RootState } from '../store';
@ -29,11 +29,11 @@ export const CONFIG_REQUEST = 'CONFIG_REQUEST';
export const CONFIG_SUCCESS = 'CONFIG_SUCCESS';
export const CONFIG_FAILURE = 'CONFIG_FAILURE';
function isObjectField(field: Field): field is BaseField & ObjectField {
function isObjectField<F extends BaseField = UnknownField>(field: Field<F>): field is ObjectField {
return 'fields' in (field as ObjectField);
}
function isFieldList(field: Field): field is BaseField & ListField {
function isFieldList<F extends BaseField = UnknownField>(field: Field<F>): field is ListField {
return 'types' in (field as ListField) || 'field' in (field as ListField);
}
@ -186,15 +186,13 @@ export function applyDefaults(originalConfig: Config) {
delete collection[I18N];
}
if (collection.fields) {
if ('fields' in collection && collection.fields) {
collection.fields = setI18nDefaultsForFields(collection.fields, Boolean(collectionI18n));
}
const { folder, files, view_filters, view_groups } = collection;
if (folder) {
collection.type = FOLDER;
const { view_filters, view_groups } = collection;
if ('folder' in collection && collection.folder) {
if (collection.path && !collection.media_folder) {
// default value for media folder when using the path config
collection.media_folder = '';
@ -204,21 +202,17 @@ export function applyDefaults(originalConfig: Config) {
collection.public_folder = collection.media_folder;
}
if (collection.fields) {
if ('fields' in collection && collection.fields) {
collection.fields = traverseFieldsJS(collection.fields, setDefaultPublicFolderForField);
}
collection.folder = trim(folder, '/');
collection.folder = trim(collection.folder, '/');
}
if (files) {
collection.type = FILES;
if ('files' in collection && collection.files) {
throwOnInvalidFileCollectionStructure(collectionI18n);
delete collection.nested;
for (const file of files) {
for (const file of collection.files) {
file.file = trimStart(file.file, '/');
if ('media_folder' in file && !('public_folder' in file)) {

View File

@ -1,9 +1,9 @@
import isEqual from 'lodash/isEqual';
import { currentBackend } from '../backend';
import { SORT_DIRECTION_ASCENDING } from '../constants';
import ValidationErrorTypes from '../constants/validationErrorTypes';
import { getSearchIntegrationProvider } from '../integrations';
import { SortDirection } from '../interface';
import { duplicateDefaultI18nFields, hasI18n, I18N_FIELD, serializeI18n } from '../lib/i18n';
import { serializeValues } from '../lib/serializeEntryValues';
import { Cursor } from '../lib/util';
@ -33,6 +33,7 @@ import type {
I18nSettings,
ImplementationMediaFile,
ObjectValue,
SortDirection,
ValueOrNestedValue,
ViewFilter,
ViewGroup,
@ -292,7 +293,7 @@ async function getAllEntries(state: RootState, collection: Collection) {
export function sortByField(
collection: Collection,
key: string,
direction: SortDirection = SortDirection.Ascending,
direction: SortDirection = SORT_DIRECTION_ASCENDING,
) {
return async (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
const state = getState();
@ -838,6 +839,10 @@ function processValue(unsafe: string) {
export function createEmptyDraft(collection: Collection, search: string) {
return async (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
if ('files' in collection) {
return;
}
const params = new URLSearchParams(search);
params.forEach((value, key) => {
collection = updateFieldByKey(collection, key, field => {
@ -885,7 +890,7 @@ export function createEmptyDraftData(
}
const subfields = 'fields' in item && item.fields;
const list = item.widget == 'list';
const list = item.widget === 'list';
const name = item.name;
const defaultValue = (('default' in item ? item.default : null) ?? null) as EntryData;

View File

@ -6,7 +6,7 @@ import { getMediaDisplayURL, getMediaFile, waitForMediaLibraryToLoad } from './m
import type { AnyAction } from 'redux';
import type { ThunkDispatch } from 'redux-thunk';
import type { Collection, Entry, Field } from '../interface';
import type { BaseField, Collection, Entry, Field, UnknownField } from '../interface';
import type { RootState } from '../store';
import type AssetProxy from '../valueObjects/AssetProxy';
@ -83,11 +83,11 @@ async function loadAsset(
const promiseCache: Record<string, Promise<AssetProxy>> = {};
export function getAsset(
export function getAsset<F extends BaseField = UnknownField>(
collection: Collection | null | undefined,
entry: Entry | null | undefined,
path: string,
field?: Field,
field?: F,
) {
return (
dispatch: ThunkDispatch<RootState, {}, AnyAction>,
@ -102,7 +102,13 @@ export function getAsset(
return Promise.resolve(emptyAsset);
}
const resolvedPath = selectMediaFilePath(state.config.config, collection, entry, path, field);
const resolvedPath = selectMediaFilePath(
state.config.config,
collection,
entry,
path,
field as Field,
);
let { asset, isLoading, error } = state.medias[resolvedPath] || {};
if (isLoading) {
return promiseCache[resolvedPath];

View File

@ -14,11 +14,13 @@ import { waitUntilWithTimeout } from './waitUntil';
import type { AnyAction } from 'redux';
import type { ThunkDispatch } from 'redux-thunk';
import type {
BaseField,
DisplayURLState,
Field,
ImplementationMediaFile,
MediaFile,
MediaLibraryInstance,
UnknownField,
} from '../interface';
import type { RootState } from '../store';
import type AssetProxy from '../valueObjects/AssetProxy';
@ -72,7 +74,7 @@ export function removeMediaControl(id: string) {
};
}
export function openMediaLibrary(
export function openMediaLibrary<F extends BaseField = UnknownField>(
payload: {
controlID?: string;
forImage?: boolean;
@ -80,17 +82,27 @@ export function openMediaLibrary(
allowMultiple?: boolean;
replaceIndex?: number;
config?: Record<string, unknown>;
field?: Field;
field?: F;
} = {},
) {
return (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
const state = getState();
const mediaLibrary = state.mediaLibrary.externalLibrary;
const { controlID, value, config = {}, allowMultiple, forImage, replaceIndex, field } = payload;
if (mediaLibrary) {
const { controlID: id, value, config = {}, allowMultiple, forImage } = payload;
mediaLibrary.show({ id, value, config, allowMultiple, imagesOnly: forImage });
mediaLibrary.show({ id: controlID, value, config, allowMultiple, imagesOnly: forImage });
}
dispatch(mediaLibraryOpened(payload));
dispatch(
mediaLibraryOpened({
controlID,
forImage,
value,
allowMultiple,
replaceIndex,
config,
field: field as Field,
}),
);
};
}

View File

@ -5,7 +5,6 @@ import get from 'lodash/get';
import isError from 'lodash/isError';
import uniq from 'lodash/uniq';
import { FILES, FOLDER } from './constants/collectionTypes';
import { resolveFormat } from './formats/formats';
import { commitMessageFormatter, slugFormatter } from './lib/formatters';
import {
@ -262,7 +261,7 @@ interface PersistArgs {
function collectionDepth(collection: Collection) {
let depth;
depth = collection.nested?.depth || getPathDepth(collection.path ?? '');
depth = 'nested' in collection && collection.nested?.depth || getPathDepth(collection.path ?? '');
if (hasI18n(collection)) {
depth = getI18nFilesDepth(collection, depth);
@ -441,20 +440,17 @@ export class Backend {
async listEntries(collection: Collection) {
const extension = selectFolderEntryExtension(collection);
let listMethod: () => Promise<ImplementationEntry[]>;
const collectionType = collection.type;
if (collectionType === FOLDER) {
if ('folder' in collection) {
listMethod = () => {
const depth = collectionDepth(collection);
return this.implementation.entriesByFolder(collection.folder as string, extension, depth);
};
} else if (collectionType === FILES) {
const files = collection.files!.map(collectionFile => ({
} else {
const files = collection.files.map(collectionFile => ({
path: collectionFile!.file,
label: collectionFile!.label,
}));
listMethod = () => this.implementation.entriesByFiles(files);
} else {
throw new Error(`Unknown collection type: ${collectionType}`);
}
const loadedEntries = await listMethod();
/*
@ -481,7 +477,7 @@ export class Backend {
// returns all the collected entries. Used to retrieve all entries
// for local searches and queries.
async listAllEntries(collection: Collection) {
if (collection.folder && this.implementation.allEntriesByFolder) {
if ('folder' in collection && collection.folder && this.implementation.allEntriesByFolder) {
const depth = collectionDepth(collection);
const extension = selectFolderEntryExtension(collection);
return this.implementation
@ -513,8 +509,8 @@ export class Backend {
// TODO: pass search fields in as an argument
let searchFields: (string | null | undefined)[] = [];
if (collection.type === FILES) {
collection.files?.forEach(f => {
if ('files' in collection) {
collection.files.forEach(f => {
const topLevelFields = f!.fields.map(f => f!.name);
searchFields = [...searchFields, ...topLevelFields];
});
@ -970,19 +966,19 @@ export class Backend {
}
fieldsOrder(collection: Collection, entry: Entry) {
const fields = collection.fields;
if (fields) {
return collection.fields.map(f => f!.name);
if ('fields' in collection) {
return collection.fields?.map(f => f!.name) ?? [];
} else {
const files = collection.files ?? [];
const file: CollectionFile | null = files.filter(f => f!.name === entry.slug)?.[0] ?? null;
if (file == null) {
throw new Error(`No file found for ${entry.slug} in ${collection.name}`);
}
return file.fields.map(f => f.name);
}
const files = collection.files;
const file: CollectionFile | null =
(files ?? []).filter(f => f!.name === entry.slug)?.[0] ?? null;
if (file == null) {
throw new Error(`No file found for ${entry.slug} in ${collection.name}`);
}
return file.fields.map(f => f.name);
return [];
}
filterEntries(collection: { entries: Entry[] }, filterRule: FilterRule) {

View File

@ -87,7 +87,10 @@ const Header = ({
}, []);
const createableCollections = useMemo(
() => Object.values(collections).filter(collection => collection.create),
() =>
Object.values(collections).filter(collection =>
'folder' in collection ? collection.create ?? false : false,
),
[collections],
);

View File

@ -10,7 +10,7 @@ import {
sortByField as sortByFieldAction,
} from '../../actions/entries';
import { components } from '../../components/UI/styles';
import { SortDirection } from '../../interface';
import { SORT_DIRECTION_ASCENDING } from '../../constants';
import { getNewEntryUrl } from '../../lib/urlHelper';
import {
selectSortableFields,
@ -31,7 +31,13 @@ import Sidebar from './Sidebar';
import type { ComponentType } from 'react';
import type { ConnectedProps } from 'react-redux';
import type { Collection, TranslatedProps, ViewFilter, ViewGroup } from '../../interface';
import type {
Collection,
SortDirection,
TranslatedProps,
ViewFilter,
ViewGroup,
} from '../../interface';
import type { RootState } from '../../store';
const CollectionMain = styled('main')`
@ -77,7 +83,7 @@ const CollectionView = ({
}, [collection]);
const newEntryUrl = useMemo(() => {
let url = collection.create ? getNewEntryUrl(collectionName) : '';
let url = 'fields' in collection && collection.create ? getNewEntryUrl(collectionName) : '';
if (url && filterTerm) {
url = getNewEntryUrl(collectionName);
if (filterTerm) {
@ -163,7 +169,7 @@ const CollectionView = ({
return;
}
const defaultSort = collection.sortable_fields.default;
const defaultSort = collection.sortable_fields?.default;
if (!defaultSort || !defaultSort.field) {
if (!readyToLoad) {
setReadyToLoad(true);
@ -177,7 +183,7 @@ const CollectionView = ({
const sortEntries = () => {
setTimeout(async () => {
await onSortClick(defaultSort.field, defaultSort.direction ?? SortDirection.Ascending);
await onSortClick(defaultSort.field, defaultSort.direction ?? SORT_DIRECTION_ASCENDING);
if (alive) {
setReadyToLoad(true);
@ -218,8 +224,8 @@ const CollectionView = ({
sortableFields={sortableFields}
onSortClick={onSortClick}
sort={sort}
viewFilters={viewFilters}
viewGroups={viewGroups}
viewFilters={viewFilters ?? []}
viewGroups={viewGroups ?? []}
t={t}
onFilterClick={onFilterClick}
onGroupClick={onGroupClick}

View File

@ -94,9 +94,10 @@ function mapStateToProps(state: RootState, ownProps: EntryCardOwnProps) {
...ownProps,
path: `/collections/${collection.name}/entries/${entry.slug}`,
image,
imageField: collection.fields?.find(
f => f.name === inferedFields.imageField && f.widget === 'image',
),
imageField:
'fields' in collection
? collection.fields?.find(f => f.name === inferedFields.imageField && f.widget === 'image')
: undefined,
isLoadingAsset,
};
}

View File

@ -165,7 +165,7 @@ export function walk(treeData: TreeNodeData[], callback: (node: TreeNodeData) =>
}
export function getTreeData(collection: Collection, entries: Entry[]): TreeNodeData[] {
const collectionFolder = collection.folder ?? '';
const collectionFolder = 'folder' in collection ? collection.folder : '';
const rootFolder = '/';
const entriesObj = entries.map(e => ({ ...e, path: e.path.slice(collectionFolder.length) }));

View File

@ -8,9 +8,13 @@ import MenuItem from '@mui/material/MenuItem';
import React, { useCallback, useMemo } from 'react';
import { translate } from 'react-polyglot';
import { SortDirection } from '../../interface';
import {
SORT_DIRECTION_ASCENDING,
SORT_DIRECTION_DESCENDING,
SORT_DIRECTION_NONE,
} from '../../constants';
import type { SortableField, SortMap, TranslatedProps } from '../../interface';
import type { SortableField, SortDirection, SortMap, TranslatedProps } from '../../interface';
const StyledMenuIconWrapper = styled('div')`
width: 32px;
@ -22,12 +26,12 @@ const StyledMenuIconWrapper = styled('div')`
function nextSortDirection(direction: SortDirection) {
switch (direction) {
case SortDirection.Ascending:
return SortDirection.Descending;
case SortDirection.Descending:
return SortDirection.None;
case SORT_DIRECTION_ASCENDING:
return SORT_DIRECTION_DESCENDING;
case SORT_DIRECTION_DESCENDING:
return SORT_DIRECTION_NONE;
default:
return SortDirection.Ascending;
return SORT_DIRECTION_NONE;
}
}
@ -53,7 +57,7 @@ const SortControl = ({ t, fields, onSortClick, sort }: TranslatedProps<SortContr
}
const sortValues = Object.values(sort);
if (Object.values(sortValues).length < 1 || sortValues[0].direction === SortDirection.None) {
if (Object.values(sortValues).length < 1 || sortValues[0].direction === SORT_DIRECTION_NONE) {
return { key: undefined, direction: undefined };
}
@ -83,7 +87,7 @@ const SortControl = ({ t, fields, onSortClick, sort }: TranslatedProps<SortContr
}}
>
{fields.map(field => {
const sortDir = sort?.[field.name]?.direction ?? SortDirection.None;
const sortDir = sort?.[field.name]?.direction ?? SORT_DIRECTION_NONE;
const nextSortDir = nextSortDirection(sortDir);
return (
<MenuItem
@ -94,7 +98,7 @@ const SortControl = ({ t, fields, onSortClick, sort }: TranslatedProps<SortContr
<ListItemText>{field.label ?? field.name}</ListItemText>
<StyledMenuIconWrapper>
{field.name === selectedSort.key ? (
selectedSort.direction === SortDirection.Ascending ? (
selectedSort.direction === SORT_DIRECTION_ASCENDING ? (
<KeyboardArrowUpIcon fontSize="small" />
) : (
<KeyboardArrowDownIcon fontSize="small" />

View File

@ -7,7 +7,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ScrollSync, ScrollSyncPane } from 'react-scroll-sync';
import { colorsRaw, components, zIndex } from '../../components/UI/styles';
import { FILES } from '../../constants/collectionTypes';
import { transientOptions } from '../../lib';
import { getI18nInfo, getPreviewEntry, hasI18n } from '../../lib/i18n';
import { getFileFromSlug } from '../../lib/util/collection.util';
@ -221,7 +220,7 @@ const EditorInterface = ({
let preview = collection.editor?.preview ?? true;
let frame = collection.editor?.frame ?? true;
if (collection.type === FILES) {
if ('files' in collection) {
const file = getFileFromSlug(collection, entry.slug);
if (file?.editor?.preview !== undefined) {
preview = file.editor.preview;

View File

@ -106,7 +106,10 @@ const EditorToolbar = ({
t,
editorBackLink,
}: TranslatedProps<EditorToolbarProps>) => {
const canCreate = useMemo(() => collection.create ?? false, [collection.create]);
const canCreate = useMemo(
() => ('folder' in collection && collection.create) ?? false,
[collection],
);
const canDelete = useMemo(() => selectAllowDeletion(collection), [collection]);
const isPublished = useMemo(() => !isNewEntry && !hasChanged, [hasChanged, isNewEntry]);

3
core/src/constants.ts Normal file
View File

@ -0,0 +1,3 @@
export const SORT_DIRECTION_ASCENDING = 'Ascending';
export const SORT_DIRECTION_DESCENDING = 'Descending';
export const SORT_DIRECTION_NONE = 'None';

View File

@ -1,4 +0,0 @@
export const FILES = 'file_based_collection';
export const FOLDER = 'folder_based_collection';
export type CollectionType = typeof FILES | typeof FOLDER;

View File

@ -1,6 +1,6 @@
import type {
EditorPlugin as MarkdownPlugin,
EditorType as MarkdownEditorType
EditorPlugin as MarkdownPlugin,
EditorType as MarkdownEditorType,
} from '@toast-ui/editor/types/editor';
import type { ToolbarItemOptions as MarkdownToolbarItemOptions } from '@toast-ui/editor/types/ui';
import type { PropertiesSchema } from 'ajv/dist/types/json-schema';
@ -8,6 +8,11 @@ import type { ComponentType, FunctionComponent, ReactNode } from 'react';
import type { t, TranslateProps as ReactPolyglotTranslateProps } from 'react-polyglot';
import type { MediaFile as BackendMediaFile } from './backend';
import type { EditorControlProps } from './components/Editor/EditorControlPane/EditorControl';
import type {
SORT_DIRECTION_ASCENDING,
SORT_DIRECTION_DESCENDING,
SORT_DIRECTION_NONE,
} from './constants';
import type { formatExtensions } from './formats/formats';
import type { I18N_STRUCTURE } from './lib/i18n';
import type { AllowedEvent } from './lib/registry';
@ -162,38 +167,47 @@ export interface i18nCollection<EF extends BaseField = UnknownField>
i18n: Required<Collection<EF>>['i18n'];
}
export interface Collection<EF extends BaseField = UnknownField> {
export interface BaseCollection {
name: string;
description?: string;
icon?: string;
folder?: string;
files?: CollectionFile<EF>[];
fields: Field<EF>[];
isFetching?: boolean;
media_folder?: string;
public_folder?: string;
summary?: string;
filter?: FilterRule;
type: 'file_based_collection' | 'folder_based_collection';
extension?: string;
format?: Format;
frontmatter_delimiter?: string | [string, string];
create?: boolean;
delete?: boolean;
identifier_field?: string;
path?: string;
slug?: string;
label_singular?: string;
label: string;
sortable_fields: SortableFields;
view_filters: ViewFilter[];
view_groups: ViewGroup[];
nested?: Nested;
sortable_fields?: SortableFields;
view_filters?: ViewFilter[];
view_groups?: ViewGroup[];
i18n?: boolean | I18nInfo;
hide?: boolean;
editor?: EditorConfig;
identifier_field?: string;
path?: string;
extension?: string;
format?: Format;
frontmatter_delimiter?: string | [string, string];
slug?: string;
media_folder?: string;
public_folder?: string;
}
export interface FilesCollection<EF extends BaseField = UnknownField> extends BaseCollection {
files: CollectionFile<EF>[];
}
export interface FolderCollection<EF extends BaseField = UnknownField> extends BaseCollection {
folder: string;
fields: Field<EF>[];
create?: boolean;
delete?: boolean;
nested?: Nested;
}
export type Collection<EF extends BaseField = UnknownField> =
| FilesCollection<EF>
| FolderCollection<EF>;
export type Collections<EF extends BaseField = UnknownField> = Record<string, Collection<EF>>;
export interface MediaLibraryInstance {
@ -220,7 +234,10 @@ export interface DisplayURLState {
export type TranslatedProps<T> = T & ReactPolyglotTranslateProps;
export type GetAssetFunction = (path: string, field?: Field) => Promise<AssetProxy>;
export type GetAssetFunction<F extends BaseField = UnknownField> = (
path: string,
field?: F,
) => Promise<AssetProxy>;
export interface WidgetControlProps<T, F extends BaseField = UnknownField> {
collection: Collection<F>;
@ -230,7 +247,7 @@ export interface WidgetControlProps<T, F extends BaseField = UnknownField> {
fieldsErrors: FieldsErrors;
submitted: boolean;
forList: boolean;
getAsset: GetAssetFunction;
getAsset: GetAssetFunction<F>;
isDisabled: boolean;
isFieldDuplicate: EditorControlProps['isFieldDuplicate'];
isFieldHidden: EditorControlProps['isFieldHidden'];
@ -250,22 +267,16 @@ export interface WidgetControlProps<T, F extends BaseField = UnknownField> {
value: T | undefined | null;
}
export interface WidgetPreviewProps<
T = unknown,
F extends BaseField = UnknownField
> {
export interface WidgetPreviewProps<T = unknown, F extends BaseField = UnknownField> {
config: Config<F>;
collection: Collection<F>;
entry: Entry;
field: RenderedField<F>;
getAsset: GetAssetFunction;
getAsset: GetAssetFunction<F>;
value: T | undefined | null;
}
export type WidgetPreviewComponent<
T = unknown,
F extends BaseField = UnknownField
> =
export type WidgetPreviewComponent<T = unknown, F extends BaseField = UnknownField> =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| React.ReactElement<unknown, string | React.JSXElementConstructor<any>>
| ComponentType<WidgetPreviewProps<T, F>>;
@ -288,14 +299,15 @@ export interface TemplatePreviewProps<T = EntryData, EF extends BaseField = Unkn
entry: Entry<T>;
document: Document | undefined | null;
window: Window | undefined | null;
getAsset: GetAssetFunction;
getAsset: GetAssetFunction<Field<EF>>;
widgetFor: (name: T extends EntryData ? string : keyof T) => ReactNode;
widgetsFor: WidgetsFor<T>;
}
export type TemplatePreviewComponent<T = EntryData, EF extends BaseField = UnknownField> = ComponentType<
TemplatePreviewProps<T, EF>
>;
export type TemplatePreviewComponent<
T = EntryData,
EF extends BaseField = UnknownField,
> = ComponentType<TemplatePreviewProps<T, EF>>;
export interface WidgetOptions<T = unknown, F extends BaseField = UnknownField> {
validator?: Widget<T, F>['validator'];
@ -490,6 +502,7 @@ export interface BaseField {
pattern?: [string, string];
i18n?: boolean | 'translate' | 'duplicate' | 'none';
comment?: string;
widget: string;
}
export interface BooleanField extends BaseField {
@ -623,7 +636,7 @@ export interface HiddenField extends BaseField {
export interface StringOrTextField extends BaseField {
// This is the default widget, so declaring its type is optional.
widget?: 'string' | 'text';
widget: 'string' | 'text';
default?: string;
}
@ -662,11 +675,10 @@ export interface ViewGroup {
pattern?: string;
}
export enum SortDirection {
Ascending = 'Ascending',
Descending = 'Descending',
None = 'None',
}
export type SortDirection =
| typeof SORT_DIRECTION_ASCENDING
| typeof SORT_DIRECTION_DESCENDING
| typeof SORT_DIRECTION_NONE;
export interface SortableFieldsDefault {
field: string;
@ -870,3 +882,8 @@ export interface MarkdownEditorOptions {
toolbarItems?: MarkdownToolbarItemsFactory;
plugins?: MarkdownPluginFactory[];
}
export enum CollectionType {
FOLDER,
FILES,
}

View File

@ -11,7 +11,7 @@ import {
addFileTemplateFields,
compileStringTemplate,
keyToPathArray,
parseDateFromEntry,
parseDateFromEntry
} from './widgets/stringTemplate';
import type { Collection, Config, Entry, EntryData, Slug } from '../interface';
@ -111,7 +111,9 @@ export function summaryFormatter(summaryTemplate: string, entry: Entry, collecti
const date = parseDateFromEntry(entry, selectInferedField(collection, 'date')) || null;
const identifier = get(entryData, keyToPathArray(selectIdentifier(collection)));
entryData = addFileTemplateFields(entry.path, entryData, collection.folder) ?? {};
entryData =
addFileTemplateFields(entry.path, entryData, 'folder' in collection ? collection.folder : '') ??
{};
// allow commit information in summary template
if (entry.author && !selectField(collection, COMMIT_AUTHOR)) {
entryData = set(entryData, COMMIT_AUTHOR, entry.author);
@ -136,7 +138,11 @@ export function folderFormatter(
}
let fields = set(entry.data, folderKey, defaultFolder) as EntryData;
fields = addFileTemplateFields(entry.path, fields, collection.folder);
fields = addFileTemplateFields(
entry.path,
fields,
'folder' in collection ? collection.folder : '',
);
const date = parseDateFromEntry(entry, selectInferedField(collection, 'date')) || null;
const identifier = get(fields, keyToPathArray(selectIdentifier(collection)));

View File

@ -22,13 +22,13 @@ export enum I18N_FIELD {
NONE = 'none',
}
export function hasI18n(collection: Collection): collection is i18nCollection {
export function hasI18n(collection: Collection | i18nCollection): collection is i18nCollection {
return I18N in collection;
}
export function getI18nInfo(collection: i18nCollection): I18nInfo;
export function getI18nInfo(collection: Collection): I18nInfo | null;
export function getI18nInfo(collection: Collection): I18nInfo | null {
export function getI18nInfo(collection: Collection | i18nCollection): I18nInfo | null {
if (!hasI18n(collection) || typeof collection[I18N] !== 'object') {
return null;
}

View File

@ -5,6 +5,7 @@ import type {
BackendClass,
BackendInitializer,
BackendInitializerOptions,
BaseField,
Config,
CustomIcon,
Entry,
@ -19,6 +20,7 @@ import type {
PreviewStyle,
PreviewStyleOptions,
TemplatePreviewComponent,
UnknownField,
Widget,
WidgetOptions,
WidgetParam,
@ -124,21 +126,21 @@ export function getPreviewTemplate(name: string): TemplatePreviewComponent<Entry
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function registerWidget(widgets: WidgetParam<any, any>[]): void;
export function registerWidget(widget: WidgetParam): void;
export function registerWidget<T = unknown>(
export function registerWidget<T = unknown, F extends BaseField = UnknownField>(
name: string,
control: string | Widget<T>['control'],
preview?: Widget<T>['preview'],
options?: WidgetOptions,
control: string | Widget<T, F>['control'],
preview?: Widget<T, F>['preview'],
options?: WidgetOptions<T, F>,
): void;
export function registerWidget<T = unknown>(
name: string | WidgetParam<T> | WidgetParam[],
control?: string | Widget<T>['control'],
preview?: Widget<T>['preview'],
export function registerWidget<T = unknown, F extends BaseField = UnknownField>(
name: string | WidgetParam<T, F> | WidgetParam[],
control?: string | Widget<T, F>['control'],
preview?: Widget<T, F>['preview'],
{
schema,
validator = () => false,
getValidValue = (value: unknown) => value,
}: WidgetOptions = {},
getValidValue = (value: T | null | undefined) => value,
}: WidgetOptions<T, F> = {},
): void {
if (Array.isArray(name)) {
name.forEach(widget => {
@ -184,12 +186,12 @@ export function registerWidget<T = unknown>(
throw Error(`Widget "${widgetName}" registered without \`controlComponent\`.`);
}
registry.widgets[widgetName] = {
control: control as Widget['control'],
preview: preview as Widget['preview'],
validator: validator as Widget['validator'],
getValidValue: getValidValue as Widget['getValidValue'],
control,
preview,
validator,
getValidValue,
schema,
};
} as unknown as Widget;
} else {
console.error('`registerWidget` failed, called with incorrect arguments.');
}

View File

@ -1,7 +1,6 @@
import get from 'lodash/get';
import { useMemo } from 'react';
import { FILES, FOLDER } from '../../constants/collectionTypes';
import { COMMIT_AUTHOR, COMMIT_DATE } from '../../constants/commitProps';
import {
IDENTIFIER_FIELDS,
@ -19,114 +18,94 @@ import type { Backend } from '../../backend';
import type { InferredField } from '../../constants/fieldInference';
import type {
Collection,
CollectionFile,
Config,
Entry,
Field,
FilesCollection,
ObjectField,
SortableField,
} from '../../interface';
const selectors = {
[FOLDER]: {
entryExtension(collection: Collection) {
return (collection.extension || formatExtensions[collection.format ?? 'frontmatter']).replace(
/^\./,
'',
);
},
fields(collection: Collection) {
return collection.fields;
},
entryPath(collection: Collection, slug: string) {
const folder = (collection.folder as string).replace(/\/$/, '');
return `${folder}/${slug}.${this.entryExtension(collection)}`;
},
entrySlug(collection: Collection, path: string) {
const folder = (collection.folder as string).replace(/\/$/, '');
const slug = path
.split(folder + '/')
.pop()
?.replace(new RegExp(`\\.${this.entryExtension(collection)}$`), '');
return slug;
},
allowNewEntries(collection: Collection) {
return collection.create;
},
allowDeletion(collection: Collection) {
return collection.delete ?? true;
},
templateName(collection: Collection) {
return collection.name;
},
},
[FILES]: {
fileForEntry(collection: Collection, slug?: string) {
const files = collection.files;
if (!slug) {
return files?.[0];
}
return files && files.filter(f => f?.name === slug)?.[0];
},
fields(collection: Collection, slug?: string) {
const file = this.fileForEntry(collection, slug);
return file && file.fields;
},
entryPath(collection: Collection, slug: string) {
const file = this.fileForEntry(collection, slug);
return file && file.file;
},
entrySlug(collection: Collection, path: string) {
const file = (collection.files as CollectionFile[]).filter(f => f?.file === path)?.[0];
return file && file.name;
},
entryLabel(collection: Collection, slug: string) {
const file = this.fileForEntry(collection, slug);
return file && file.label;
},
allowNewEntries() {
return false;
},
allowDeletion(collection: Collection) {
return collection.delete ?? false;
},
templateName(_collection: Collection, slug: string) {
return slug;
},
},
};
function fileForEntry(collection: FilesCollection, slug?: string) {
const files = collection.files;
if (!slug) {
return files?.[0];
}
return files && files.filter(f => f?.name === slug)?.[0];
}
export function selectFields(collection: Collection, slug?: string) {
return selectors[collection.type].fields(collection, slug);
if ('fields' in collection) {
return collection.fields;
}
const file = fileForEntry(collection, slug);
return file && file.fields;
}
export function selectFolderEntryExtension(collection: Collection) {
return selectors[FOLDER].entryExtension(collection);
return (collection.extension || formatExtensions[collection.format ?? 'frontmatter']).replace(
/^\./,
'',
);
}
export function selectFileEntryLabel(collection: Collection, slug: string) {
return selectors[FILES].entryLabel(collection, slug);
if ('fields' in collection) {
return undefined;
}
const file = fileForEntry(collection, slug);
return file && file.label;
}
export function selectEntryPath(collection: Collection, slug: string) {
return selectors[collection.type].entryPath(collection, slug);
if ('fields' in collection) {
const folder = collection.folder.replace(/\/$/, '');
return `${folder}/${slug}.${selectFolderEntryExtension(collection)}`;
}
const file = fileForEntry(collection, slug);
return file && file.file;
}
export function selectEntrySlug(collection: Collection, path: string) {
return selectors[collection.type].entrySlug(collection, path);
if ('fields' in collection) {
const folder = (collection.folder as string).replace(/\/$/, '');
const slug = path
.split(folder + '/')
.pop()
?.replace(new RegExp(`\\.${selectFolderEntryExtension(collection)}$`), '');
return slug;
}
const file = collection.files.filter(f => f?.file === path)?.[0];
return file && file.name;
}
export function selectAllowNewEntries(collection: Collection) {
return selectors[collection.type].allowNewEntries(collection);
if ('fields' in collection) {
return collection.create ?? true;
}
return false;
}
export function selectAllowDeletion(collection: Collection) {
return selectors[collection.type].allowDeletion(collection);
if ('fields' in collection) {
return collection.delete ?? true;
}
return false;
}
export function selectTemplateName(collection: Collection, slug: string) {
return selectors[collection.type].templateName(collection, slug);
if ('fields' in collection) {
return collection.name;
}
return slug;
}
export function selectEntryCollectionTitle(collection: Collection, entry: Entry): string {
@ -137,7 +116,7 @@ export function selectEntryCollectionTitle(collection: Collection, entry: Entry)
}
// if the collection is a file collection return the label of the entry
if (collection.type == FILES) {
if ('files' in collection && collection.files) {
const label = selectFileEntryLabel(collection, entry.slug);
if (label) {
return label;
@ -250,7 +229,7 @@ function getFieldsWithMediaFolders(fields: Field[]) {
return fieldsWithMediaFolders;
}
export function getFileFromSlug(collection: Collection, slug: string) {
export function getFileFromSlug(collection: FilesCollection, slug: string) {
return collection.files?.find(f => f.name === slug);
}
@ -258,7 +237,7 @@ export function selectFieldsWithMediaFolders(collection: Collection, slug: strin
if ('folder' in collection) {
const fields = collection.fields;
return getFieldsWithMediaFolders(fields);
} else if ('files' in collection) {
} else {
const fields = getFileFromSlug(collection, slug)?.fields || [];
return getFieldsWithMediaFolders(fields);
}
@ -274,17 +253,15 @@ export function selectMediaFolders(config: Config, collection: Collection, entry
if (file) {
folders.unshift(selectMediaFolder(config, collection, entry, undefined));
}
}
if ('media_folder' in collection) {
} else if ('media_folder' in collection) {
// stop evaluating media folders at collection level
const newCollection = { ...collection };
delete newCollection.files;
folders.unshift(selectMediaFolder(config, newCollection, entry, undefined));
}
return [...new Set(...folders)];
}
export function getFieldsNames(fields: (Field | Field)[] | undefined, prefix = '') {
export function getFieldsNames(fields: Field[] | undefined, prefix = '') {
let names = fields?.map(f => `${prefix}${f.name}`) ?? [];
fields?.forEach((f, index) => {
@ -347,7 +324,9 @@ export function updateFieldByKey(
}
}
collection.fields = traverseFields(collection.fields ?? [], updateAndBreak, () => updated);
if ('fields' in collection) {
collection.fields = traverseFields(collection.fields ?? [], updateAndBreak, () => updated);
}
return collection;
}
@ -355,7 +334,7 @@ export function updateFieldByKey(
export function selectIdentifier(collection: Collection) {
const identifier = collection.identifier_field;
const identifierFields = identifier ? [identifier, ...IDENTIFIER_FIELDS] : [...IDENTIFIER_FIELDS];
const fieldNames = getFieldsNames(collection.fields ?? []);
const fieldNames = getFieldsNames('fields' in collection ? collection.fields ?? [] : []);
return identifierFields.find(id =>
fieldNames.find(name => name.toLowerCase().trim() === id.toLowerCase().trim()),
);
@ -377,7 +356,7 @@ export function selectInferedField(collection: Collection, fieldName: string) {
}
>
)[fieldName];
const fields = collection.fields as (Field | Field)[];
const fields = 'fields' in collection ? collection.fields ?? [] : [];
let field;
// If collection has no fields or fieldName is not defined within inferables list, return null

View File

@ -6,15 +6,18 @@ import type { Collection, Field } from '../../interface';
export function selectField(collection: Collection, key: string) {
const array = keyToPathArray(key);
let name: string | undefined;
let field;
let fields = collection.fields ?? [];
while ((name = array.shift()) && fields) {
field = fields.find(f => f.name === name);
if (field) {
if ('fields' in field) {
fields = field?.fields ?? [];
} else if ('types' in field) {
fields = field?.types ?? [];
let field: Field | undefined;
if ('fields' in collection) {
let fields = collection.fields ?? [];
while ((name = array.shift()) && fields) {
field = fields.find(f => f.name === name);
if (field) {
if ('fields' in field) {
fields = field?.fields ?? [];
} else if ('types' in field) {
fields = field?.types ?? [];
}
}
}
}

View File

@ -49,7 +49,7 @@ function hasCustomFolder(
return true;
}
if (collection.files) {
if ('files' in collection) {
const file = getFileField(collection.files, slug);
if (file && file[folderKey]) {
return true;
@ -78,7 +78,7 @@ function evaluateFolder(
collection[folderKey] = `{{${folderKey}}}`;
}
if (collection.files) {
if ('files' in collection) {
// files collection evaluate the collection template
// then move on to the specific file configuration denoted by the slug
currentFolder = folderFormatter(
@ -244,7 +244,7 @@ export function selectMediaFolder(
const entryPath = entryMap?.path;
mediaFolder = entryPath
? join(dirname(entryPath), folder)
: join(collection!.folder as string, DRAFT_MEDIA_FILES);
: join(collection && 'folder' in collection ? collection.folder : '', DRAFT_MEDIA_FILES);
}
}

View File

@ -21,11 +21,11 @@ import {
GROUP_ENTRIES_SUCCESS,
SORT_ENTRIES_FAILURE,
SORT_ENTRIES_REQUEST,
SORT_ENTRIES_SUCCESS,
SORT_ENTRIES_SUCCESS
} from '../actions/entries';
import { SEARCH_ENTRIES_SUCCESS } from '../actions/search';
import { SORT_DIRECTION_ASCENDING, SORT_DIRECTION_NONE } from '../constants';
import { VIEW_STYLE_LIST } from '../constants/collectionViews';
import { SortDirection } from '../interface';
import { set } from '../lib/util/object.util';
import { selectSortDataPath } from '../lib/util/sort.util';
@ -44,7 +44,7 @@ import type {
Pages,
Sort,
SortMap,
SortObject,
SortObject
} from '../interface';
import type { EntryDraftState } from './entryDraft';
@ -568,7 +568,7 @@ export function selectEntriesGroupField(entries: EntriesState, collection: strin
export function selectEntriesSortFields(entries: EntriesState, collection: string) {
const sort = selectEntriesSort(entries, collection);
const values = Object.values(sort ?? {}).filter(v => v?.direction !== SortDirection.None) || [];
const values = Object.values(sort ?? {}).filter(v => v?.direction !== SORT_DIRECTION_NONE) || [];
return values;
}
@ -605,7 +605,7 @@ export function selectEntries(state: EntriesState, collection: Collection) {
const sortFields = selectEntriesSortFields(state, collectionName);
if (sortFields && sortFields.length > 0) {
const keys = sortFields.map(v => selectSortDataPath(collection, v.key));
const orders = sortFields.map(v => (v.direction === SortDirection.Ascending ? 'asc' : 'desc'));
const orders = sortFields.map(v => (v.direction === SORT_DIRECTION_ASCENDING ? 'asc' : 'desc'));
entries = orderBy(entries, keys, orders);
}

View File

@ -1,13 +0,0 @@
declare module 'tomlify-j0.4' {
interface ToTomlOptions {
replace?(key: string, value: unknown): string | false;
sort?(a: string, b: string): number;
}
interface Tomlify {
toToml(data: object, options?: ToTomlOptions): string;
}
const tomlify: Tomlify;
export default tomlify;
}

View File

@ -7,7 +7,7 @@ import type { FileOrImageField, GetAssetFunction, WidgetPreviewProps } from '../
interface FileLinkProps {
value: string;
getAsset: GetAssetFunction;
getAsset: GetAssetFunction<FileOrImageField>;
field: FileOrImageField;
}
@ -40,7 +40,7 @@ const StyledFileLink = styled(FileLink)`
interface FileLinkListProps {
values: string[];
getAsset: GetAssetFunction;
getAsset: GetAssetFunction<FileOrImageField>;
field: FileOrImageField;
}

View File

@ -126,7 +126,7 @@ const SortableImageButtons = ({ onRemove, onReplace }: SortableImageButtonsProps
interface SortableImageProps {
itemValue: string;
getAsset: GetAssetFunction;
getAsset: GetAssetFunction<FileOrImageField>;
field: FileOrImageField;
onRemove: MouseEventHandler;
onReplace: MouseEventHandler;
@ -167,7 +167,7 @@ const StyledSortableMultiImageWrapper = styled('div')`
interface SortableMultiImageWrapperProps {
items: string[];
getAsset: GetAssetFunction;
getAsset: GetAssetFunction<FileOrImageField>;
field: FileOrImageField;
onRemoveOne: (index: number) => MouseEventHandler;
onReplaceOne: (index: number) => MouseEventHandler;

View File

@ -18,7 +18,7 @@ const StyledImage = styled(({ src }: StyledImageProps) => (
`;
interface ImageAssetProps {
getAsset: GetAssetFunction;
getAsset: GetAssetFunction<FileOrImageField>;
value: string;
field: FileOrImageField;
}

View File

@ -0,0 +1,11 @@
import React from 'react';
import WidgetPreviewContainer from '../../components/UI/WidgetPreviewContainer';
import type { ListField, ObjectValue, WidgetPreviewProps } from '../../interface';
function ObjectPreview({ field }: WidgetPreviewProps<ObjectValue[], ListField>) {
return <WidgetPreviewContainer>{field.fields ?? null}</WidgetPreviewContainer>;
}
export default ObjectPreview;

View File

@ -1,5 +1,5 @@
import ObjectPreview from '../object/ObjectPreview';
import controlComponent from './ListControl';
import previewComponent from './ListPreview';
import schema from './schema';
import type { ListField, WidgetParam, ObjectValue } from '../../interface';
@ -8,7 +8,7 @@ const ListWidget = (): WidgetParam<ObjectValue[], ListField> => {
return {
name: 'list',
controlComponent,
previewComponent: ObjectPreview,
previewComponent,
options: {
schema,
},

View File

@ -5,7 +5,7 @@ import type AssetProxy from '../../../valueObjects/AssetProxy';
interface UseMediaProps {
value: string | undefined | null;
getAsset: GetAssetFunction;
getAsset: GetAssetFunction<MarkdownField>;
field: MarkdownField;
}

View File

@ -7,7 +7,7 @@ import Outline from '../../components/UI/Outline';
import { transientOptions } from '../../lib';
import { compileStringTemplate } from '../../lib/widgets/stringTemplate';
import type { ListField, ObjectField, ObjectValue, WidgetControlProps } from '../../interface';
import type { ObjectField, ObjectValue, WidgetControlProps } from '../../interface';
const StyledObjectControlWrapper = styled('div')`
position: relative;
@ -59,7 +59,7 @@ const ObjectControl = ({
i18n,
hasErrors,
value = {},
}: WidgetControlProps<ObjectValue, ObjectField | ListField>) => {
}: WidgetControlProps<ObjectValue, ObjectField>) => {
const [collapsed, setCollapsed] = useState(false);
const handleCollapseToggle = useCallback(() => {

View File

@ -2,11 +2,9 @@ import React from 'react';
import WidgetPreviewContainer from '../../components/UI/WidgetPreviewContainer';
import type { WidgetPreviewProps, ObjectField, ListField, ObjectValue } from '../../interface';
import type { ObjectField, ObjectValue, WidgetPreviewProps } from '../../interface';
function ObjectPreview({
field,
}: WidgetPreviewProps<ObjectValue | ObjectValue[], ObjectField | ListField>) {
function ObjectPreview({ field }: WidgetPreviewProps<ObjectValue, ObjectField>) {
return <WidgetPreviewContainer>{field.fields ?? null}</WidgetPreviewContainer>;
}

View File

@ -2,9 +2,9 @@ import controlComponent from './ObjectControl';
import previewComponent from './ObjectPreview';
import schema from './schema';
import type { ListField, ObjectField, WidgetParam, ObjectValue } from '../../interface';
import type { ObjectField, ObjectValue, WidgetParam } from '../../interface';
const ObjectWidget = (): WidgetParam<ObjectValue, ObjectField | ListField> => {
const ObjectWidget = (): WidgetParam<ObjectValue, ObjectField> => {
return {
name: 'object',
controlComponent,

File diff suppressed because it is too large Load Diff

View File

@ -4,11 +4,11 @@ title: Bundling
weight: 5
---
This tutorial guides you through the steps for adding Static CMS via a package manager to a site that's built with a common [static site generator](https://www.staticgen.com/), like Jekyll, Nest, Hugo, Hexo, or Gatsby. Alternatively, you can [start from a template](/docs/start-with-a-template) or dive right into [configuration options](/docs/configuration-options).
This tutorial guides you through the steps for adding Static CMS via a package manager to a site that's built with a common [static site generator](https://www.staticgen.com/). If you want to start form a template, the [Next Template](docs/start-with-a-template) provides a great example of bundling in action.
## Installation
You can also use Static CMS as an npm module. Wherever you import Static CMS, it automatically runs, taking over the current page. Make sure the script that imports it only runs on your CMS page. First install the package and save it to your project:
To get started you need to install Static CMS via a package manager and save it to your project:
```bash
// npm
@ -22,21 +22,33 @@ Then import it (assuming your project has tooling for imports):
```js
import CMS from '@staticcms/core';
import config from './config';
// Initialize the CMS object
CMS.init();
CMS.init({ config });
// Now the registry is available via the CMS object.
CMS.registerPreviewTemplate('my-template', MyTemplate);
```
**Note**: Wherever you initialize Static CMS (via `CMS.init()`), it takes over the current page. Make sure you only run the initialization code on your CMS page.
## Configuration
Configuration is different for every site, so we'll break it down into parts. Add all the code snippets in this section to your `admin/config.yml` file.
Configuration is different for every site, so we'll break it down into parts. Add all the code snippets in this section to your `admin/config.js` file (which is passed into the `CMS.init({ config })` call). Alternatively, you can use a yaml file (`admin/config.yml`) instead of a javascript file.
### Backend
We're using [Netlify](https://www.netlify.com) for our hosting and authentication in this tutorial, so backend configuration is fairly straightforward.
For GitHub and GitLab repositories, you can start your Static CMS `config.yml` file with these lines:
For GitHub and GitLab repositories, you can start your Static CMS `config` file with these lines:
<CodeTabs>
```js
backend: {
name: 'git-gateway',
branch: 'main' // Branch to update (optional; defaults to main)
},
```
```yaml
backend:
@ -44,6 +56,8 @@ backend:
branch: main # Branch to update (optional; defaults to main)
```
</CodeTabs>
_(For Bitbucket repositories, use the [Bitbucket backend](/docs/bitbucket-backend) instructions instead.)_
The configuration above specifies your backend protocol and your publication branch. Git Gateway is an open source API that acts as a proxy between authenticated users of your site and your site repo. (We'll get to the details of that in the [Authentication section](#authentication) below.) If you leave out the `branch` declaration, it defaults to `main`.
@ -52,21 +66,36 @@ The configuration above specifies your backend protocol and your publication bra
Static CMS allows users to upload images directly within the editor. For this to work, the CMS needs to know where to save them. If you already have an `images` folder in your project, you could use its path, possibly creating an `uploads` sub-folder, for example:
<CodeTabs>
```js
media_folder: 'images/uploads', // Media files will be stored in the repo under images/uploads
```
```yaml
# This line should *not* be indented
media_folder: 'images/uploads' # Media files will be stored in the repo under images/uploads
```
</CodeTabs>
If you're creating a new folder for uploaded media, you'll need to know where your static site generator expects static files. You can refer to the paths outlined above in [App File Structure](#app-file-structure), and put your media folder in the same location where you put the `admin` folder.
Note that the`media_folder` file path is relative to the project root, so the example above would work for Jekyll, GitBook, or any other generator that stores static files at the project root. However, it would not work for Hugo, Hexo, Middleman or others that store static files in a subfolder. Here's an example that could work for a Hugo site:
<CodeTabs>
```js
media_folder: 'static/images/uploads', // Media files will be stored in the repo under static/images/uploads
public_folder: '/images/uploads', // The src attribute for uploaded media will begin with /images/uploads
```
```yaml
# These lines should *not* be indented
media_folder: 'static/images/uploads' # Media files will be stored in the repo under static/images/uploads
public_folder: '/images/uploads' # The src attribute for uploaded media will begin with /images/uploads
```
</CodeTabs>
The configuration above adds a new setting, `public_folder`. While `media_folder` specifies where uploaded files are saved in the repo, `public_folder` indicates where they are found in the published site. Image `src` attributes use this path, which is relative to the file where it's called. For this reason, we usually start the path at the site root, using the opening `/`.
_If `public_folder` is not set, Static CMS defaults to the same value as `media_folder`, adding an opening `/` if one is not included._
@ -88,7 +117,28 @@ rating: 5
This is the post body, where I write about our last chance to party before the Y2K bug destroys us all.
```
Given this example, our `collections` settings would look like this in your Static CMS `config.yml` file:
Given this example, our `collections` settings would look like this in your Static CMS `config` file:
<CodeTabs>
```js
collections: [
{
name: 'blog', // Used in routes, e.g., /admin/collections/blog
label: 'Blog', // Used in the UI
folder: '_posts/blog', // The path to the folder where the documents are stored
create: true, // Allow users to create new documents in this collection
slug: '{{year}}-{{month}}-{{day}}-{{slug}}', // Filename template, e.g., YYYY-MM-DD-title.md
fields: [ // The fields for each document, usually in front matter
{ label: 'Layout', name: 'layout', widget: 'hidden', default: 'blog' },
{ label: 'Title', name: 'title', widget: 'string' },
{ label: 'Publish Date', name: 'date', widget: 'datetime' },
{ label: 'Featured Image', name: 'thumbnail', widget: 'image' },
{ label: 'Rating (scale of 1-5)', name: 'rating', widget: 'number' },
{ label: 'Body', name: 'body', widget: 'markdown' },
],
},
],
```
```yaml
collections:
@ -106,37 +156,63 @@ collections:
- { label: 'Body', name: 'body', widget: 'markdown' }
```
</CodeTabs>
Let's break that down:
|Field|Description|
| ---- | --------------------- |
|name|Post type identifier, used in routes. Must be unique.|
|label|What the admin UI calls the post type.|
|folder|Where files of this type are stored, relative to the repo root.|
|create|Set to `true` to allow users to create new files in this collection.|
|slug|Template for filenames. `{{ year }}`, `{{ month }}`, and `{{ day }}` pulls from the post's `date` field or save date. `{{ slug }}` is a url-safe version of the post's `title`. Default is simply `{{ slug }}`.|
|fields|Fields listed here are shown as fields in the content editor, then saved as front matter at the beginning of the document (except for `body`, which follows the front matter).|
| Field | Description |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | Post type identifier, used in routes. Must be unique. |
| label | What the admin UI calls the post type. |
| folder | Where files of this type are stored, relative to the repo root. |
| create | Set to `true` to allow users to create new files in this collection. |
| slug | Template for filenames. `{{ year }}`, `{{ month }}`, and `{{ day }}` pulls from the post's `date` field or save date. `{{ slug }}` is a url-safe version of the post's `title`. Default is simply `{{ slug }}`. |
| fields | Fields listed here are shown as fields in the content editor, then saved as front matter at the beginning of the document (except for `body`, which follows the front matter). |
As described above, the `widget` property specifies a built-in or custom UI widget for a given field. When a content editor enters a value into a widget, that value is saved in the document front matter as the value for the `name` specified for that field. A full listing of available widgets can be found in the [Widgets doc](/docs/widgets).
Based on this example, you can go through the post types in your site and add the appropriate settings to your Static CMS `config.yml` file. Each post type should be listed as a separate node under the `collections` field. See the [Collections reference doc](/docs/collection-overview) for more configuration options.
Based on this example, you can go through the post types in your site and add the appropriate settings to your Static CMS `config` file. Each post type should be listed as a separate node under the `collections` field. See the [Collections reference doc](/docs/collection-overview) for more configuration options.
### Filter
The entries for any collection can be filtered based on the value of a single field. The example collection below only shows post entries with the value `en` in the `language` field.
<CodeTabs>
```js
collections: [
{
name: 'posts',
label: 'Post',
folder: '_posts',
filter: {
field: 'language',
value: 'en',
},
fields: [
{
name: 'language',
label: 'Language',
},
],
},
],
```
```yaml
collections:
- name: 'posts'
label: 'Post'
folder: '_posts'
- name: posts
label: Post
folder: _posts
filter:
field: language
value: en
fields:
- { label: 'Language', name: 'language' }
- name: language
label: Language
```
</CodeTabs>
## Authentication
Now that you have your Static CMS files in place and configured, all that's left is to enable authentication. We're using the [Netlify](https://www.netlify.com/) platform here because it's one of the quickest ways to get started, but you can learn about other authentication options in the [Backends](/docs/backends-overview) doc.
@ -190,6 +266,6 @@ If you set your registration preference to "Invite only," invite yourself (and a
If you left your site registration open, or for return visits after confirming an email invitation, access your site's CMS at `yoursite.com/admin/`.
**Note:** No matter where you access Static CMS — whether running locally, in a staging environment, or in your published site — it always fetches and commits files in your hosted repository (for example, on GitHub), on the branch you configured in your Static CMS config.yml file. This means that content fetched in the admin UI matches the content in the repository, which may be different from your locally running site. It also means that content saved using the admin UI saves directly to the hosted repository, even if you're running the UI locally or in staging.
**Note:** No matter where you access Static CMS — whether running locally, in a staging environment, or in your published site — it always fetches and commits files in your hosted repository (for example, on GitHub), on the branch you configured in your Static CMS `config` file. This means that content fetched in the admin UI matches the content in the repository, which may be different from your locally running site. It also means that content saved using the admin UI saves directly to the hosted repository, even if you're running the UI locally or in staging.
Happy posting!

View File

@ -61,20 +61,30 @@ In the code above the `script` is loaded from the `unpkg` CDN. Should there be a
## Configuration
Configuration is different for every site, so we'll break it down into parts. Add all the code snippets in this section to your `admin/config.yml` file.
Configuration is different for every site, so we'll break it down into parts. Add all the code snippets in this section to your `admin/config.yml` file. Alternatively, you can use a javascript file (`admin/config.js`) instead of a yaml file. Simply import the javascript config and pass it into your `CMS.init({ config })` call.
### Backend
We're using [Netlify](https://www.netlify.com) for our hosting and authentication in this tutorial, so backend configuration is fairly straightforward.
For GitHub repositories, you can start your Static CMS `config.yml` file with these lines:
For GitHub repositories, you can start your Static CMS `config` file with these lines:
<CodeTabs>
```yaml
backend:
name: git-gateway
branch: main # Branch to update (optional; defaults to main)
```
```js
backend: {
name: 'git-gateway',
branch: 'main' // Branch to update (optional; defaults to main)
},
```
</CodeTabs>
_(For GitLab repositories, use [GitLab backend](/docs/gitlab-backend) and for Bitbucket repositories, use [Bitbucket backend](/docs/bitbucket-backend).)_
The configuration above specifies your backend protocol and your publication branch. Git Gateway is an open source API that acts as a proxy between authenticated users of your site and your site repo. (We'll get to the details of that in the [Authentication section](#authentication) below.) If you leave out the `branch` declaration, it defaults to `main`.
@ -83,21 +93,36 @@ The configuration above specifies your backend protocol and your publication bra
Static CMS allows users to upload images directly within the editor. For this to work, the CMS needs to know where to save them. If you already have an `images` folder in your project, you could use its path, possibly creating an `uploads` sub-folder, for example:
<CodeTabs>
```yaml
# This line should *not* be indented
media_folder: 'images/uploads' # Media files will be stored in the repo under images/uploads
```
```js
media_folder: 'images/uploads', // Media files will be stored in the repo under images/uploads
```
</CodeTabs>
If you're creating a new folder for uploaded media, you'll need to know where your static site generator expects static files. You can refer to the paths outlined above in [App File Structure](#app-file-structure), and put your media folder in the same location where you put the `admin` folder.
Note that the `media_folder` file path is relative to the project root, so the example above would work for Jekyll, GitBook, or any other generator that stores static files at the project root. However, it would not work for Hugo, Next, Hexo, Middleman or others that store static files in a subfolder. Here's an example that could work for a Hugo site:
<CodeTabs>
```yaml
# These lines should *not* be indented
media_folder: 'static/images/uploads' # Media files will be stored in the repo under static/images/uploads
public_folder: '/images/uploads' # The src attribute for uploaded media will begin with /images/uploads
```
```js
media_folder: 'static/images/uploads', // Media files will be stored in the repo under static/images/uploads
public_folder: '/images/uploads', // The src attribute for uploaded media will begin with /images/uploads
```
</CodeTabs>
The configuration above adds a new setting, `public_folder`. While `media_folder` specifies where uploaded files are saved in the repo, `public_folder` indicates where they are found in the published site. Image `src` attributes use this path, which is relative to the file where it's called. For this reason, we usually start the path at the site root, using the opening `/`.
_If `public_folder` is not set, Static CMS defaults to the same value as `media_folder`, adding an opening `/` if one is not included._
@ -119,8 +144,9 @@ rating: 5
This is the post body, where I write about our last chance to party before the Y2K bug destroys us all.
```
Given this example, our `collections` settings would look like this in your Static CMS `config.yml` file:
Given this example, our `collections` settings would look like this in your Static CMS `config` file:
<CodeTabs>
```yaml
collections:
- name: 'blog' # Used in routes, e.g., /admin/collections/blog
@ -137,6 +163,29 @@ collections:
- { label: 'Body', name: 'body', widget: 'markdown' }
```
```js
collections: [
{
name: 'blog', // Used in routes, e.g., /admin/collections/blog
label: 'Blog', // Used in the UI
folder: '_posts/blog', // The path to the folder where the documents are stored
create: true, // Allow users to create new documents in this collection
slug: '{{year}}-{{month}}-{{day}}-{{slug}}', // Filename template, e.g., YYYY-MM-DD-title.md
fields: [
// The fields for each document, usually in front matter
{ label: 'Layout', name: 'layout', widget: 'hidden', default: 'blog' },
{ label: 'Title', name: 'title', widget: 'string' },
{ label: 'Publish Date', name: 'date', widget: 'datetime' },
{ label: 'Featured Image', name: 'thumbnail', widget: 'image' },
{ label: 'Rating (scale of 1-5)', name: 'rating', widget: 'number' },
{ label: 'Body', name: 'body', widget: 'markdown' },
],
},
],
```
</CodeTabs>
Let's break that down:
| Field | Description |
@ -150,7 +199,47 @@ Let's break that down:
As described above, the `widget` property specifies a built-in or custom UI widget for a given field. When a content editor enters a value into a widget, that value is saved in the document front matter as the value for the `name` specified for that field. A full listing of available widgets can be found in the [Widgets doc](/docs/widgets).
Based on this example, you can go through the post types in your site and add the appropriate settings to your Static CMS `config.yml` file. Each post type should be listed as a separate node under the `collections` field. See the [Collections reference doc](/docs/collection-overview) for more configuration options.
Based on this example, you can go through the post types in your site and add the appropriate settings to your Static CMS `config` file. Each post type should be listed as a separate node under the `collections` field. See the [Collections reference doc](/docs/collection-overview) for more configuration options.
### Filter
The entries for any collection can be filtered based on the value of a single field. The example collection below only shows post entries with the value `en` in the `language` field.
<CodeTabs>
```yaml
collections:
- name: posts
label: Post
folder: _posts
filter:
field: language
value: en
fields:
- name: language
label: Language
```
```js
collections: [
{
name: 'posts',
label: 'Post',
folder: '_posts',
filter: {
field: 'language',
value: 'en',
},
fields: [
{
name: 'language',
label: 'Language',
},
],
},
],
```
</CodeTabs>
## Authentication
@ -205,6 +294,6 @@ If you set your registration preference to "Invite only," invite yourself (and a
If you left your site registration open, or for return visits after confirming an email invitation, access your site's CMS at `yoursite.com/admin/`.
**Note:** No matter where you access Static CMS — whether running locally, in a staging environment, or in your published site — it always fetches and commits files in your hosted repository (for example, on GitHub), on the branch you configured in your Static CMS config.yml file. This means that content fetched in the admin UI matches the content in the repository, which may be different from your locally running site. It also means that content saved using the admin UI saves directly to the hosted repository, even if you're running the UI locally or in staging.
**Note:** No matter where you access Static CMS — whether running locally, in a staging environment, or in your published site — it always fetches and commits files in your hosted repository (for example, on GitHub), on the branch you configured in your Static CMS `config` file. This means that content fetched in the admin UI matches the content in the repository, which may be different from your locally running site. It also means that content saved using the admin UI saves directly to the hosted repository, even if you're running the UI locally or in staging.
Happy posting!

View File

@ -4,9 +4,16 @@ title: Add to Your Site
weight: 3
---
You can adapt Static CMS to a wide variety of projects. It works with any content written in markdown, JSON, YAML, or TOML files, stored in a repo on [GitHub](https://github.com/), [GitLab](https://gitlab.com/) or [Bitbucket](https://bitbucket.org). You can also create your own custom backend.
You can adapt Static CMS to a wide variety of projects. It works with any content written in markdown, JSON or YAML files, stored in a repo on [GitHub](https://github.com/), [GitLab](https://gitlab.com/) or [Bitbucket](https://bitbucket.org). You can also create your own custom backend.
You can add Static CMS to your site in two different ways:
- [CDN hosting](/docs/add-to-your-site-cdn)
- [bundling directly into your app](/docs/add-to-your-site-bundling)
## CDN hosting
Adding Static CMS via a public CDN to a site that's built with a common static site generator, like Jekyll, Hugo, Hexo, or Gatsby. Alternatively, is a quick and easy way to get started. You can start from a [template](/docs/start-with-a-template) or use [this guide](/docs/add-to-your-site-cdn) to get started.
## Bundling
An alternative to CDN is directly bundling the StaticCMS package directly into your app. While this require extra setup steps, it give you the greatest level of control over your CMS. It also provides complete support for TypeScript, including for your config file.
See [the bundling guide](/docs/add-to-your-site-bundling) to get started.

View File

@ -15,6 +15,8 @@ Configuring the CMS for i18n support requires top level configuration, collectio
### Top level configuration
<CodeTabs>
```yaml
i18n:
# Required and can be one of multiple_folders, multiple_files or single_file
@ -23,16 +25,43 @@ i18n:
# single_file - persists a single file in `<folder>/<slug>.<extension>`
structure: multiple_folders
# Required - a list of locales to show in the editor UI
locales: [en, de, fr]
# Required - a list of locales to show in the editor UI
# Optional, defaults to the first item in locales.
# The locale to be used for fields validation and as a baseline for the entry.
defaultLocale: en
locales: [en, de, fr]
# Optional, defaults to the first item in locales.
# The locale to be used for fields validation and as a baseline for the entry.
defaultLocale: en
```
```js
i18n: {
/**
* Required and can be one of multiple_folders, multiple_files or single_file
* multiple_folders - persists files in `<folder>/<locale>/<slug>.<extension>`
* multiple_files - persists files in `<folder>/<slug>.<locale>.<extension>`
* single_file - persists a single file in `<folder>/<slug>.<extension>`
*/
structure: 'multiple_folders',
// Required - a list of locales to show in the editor UI
locales: ['en', 'de', 'fr'],
/**
* Optional, defaults to the first item in locales.
* The locale to be used for fields validation and as a baseline for the entry.
*/
defaultLocale: 'en'
},
```
</CodeTabs>
### Collection level configuration
<CodeTabs>
```yaml
collections:
- name: i18n_content
@ -41,8 +70,24 @@ collections:
i18n: true
```
```js
collections: [
{
name: 'i18n_content',
/**
* same as the top level, but all fields are optional and defaults to the top level
* can also be a boolean to accept the top level defaults
*/
i18n: true
},
],
```
</CodeTabs>
When using a file collection, you must also enable i18n for each individual file:
<CodeTabs>
```yaml
collections:
- name: pages
@ -61,8 +106,37 @@ collections:
- { label: Title, name: title, widget: string, i18n: true }
```
```js
collections: [
{
name: 'pages',
label: 'Pages',
// Configure i18n for this collection.
i18n: {
structure: 'single_file',
locales: ['en', 'de', 'fr']
},
files: [
{
name: 'about',
label: 'About Page',
file: 'site/content/about.yml',
// Enable i18n for this file.
i18n: true,
fields: [
{ label: 'Title', name: 'title', widget: 'string', i18n: true }
],
},
],
},
],
```
</CodeTabs>
### Field level configuration
<CodeTabs>
```yaml
fields:
- label: Title
@ -81,8 +155,36 @@ fields:
widget: markdown
```
```js
fields: [
{
label: 'Title',
name: 'title',
widget: 'string',
// same as 'i18n: translate'. Allows translation of the title field
i18n: true
},
{
label: 'Date',
name: 'date',
widget: 'datetime',
// The date field will be duplicated from the default locale.
i18n: 'duplicate'
},
{
label: 'Body',
name: 'body',
// The markdown field will be omitted from the translation.
widget: 'markdown'
},
],
```
</CodeTabs>
Example configuration:
<CodeTabs>
```yaml
i18n:
structure: multiple_folders
@ -108,12 +210,36 @@ collections:
widget: markdown
```
```js
i18n: {
structure: 'multiple_folders',
locales: ['en', 'de', 'fr']
},
collections: [
{
name: 'posts',
label: 'Posts',
folder: 'content/posts',
create: true,
i18n: true,
fields: [
{ label: 'Title', name: 'title', widget: 'string', i18n: true },
{ label: 'Date', name: 'date', widget: 'datetime', i18n: 'duplicate' },
{ label: 'Body', name: 'body', widget: 'markdown' },
],
},
],
```
</CodeTabs>
### Limitations
1. File collections support only `structure: single_file`.
2. List widgets only support `i18n: true`. `i18n` configuration on sub fields is ignored.
3. Object widgets only support `i18n: true` and `i18n` configuration should be done per field:
<CodeTabs>
```yaml
- label: 'Object'
name: 'object'
@ -132,6 +258,29 @@ collections:
}
```
```js
{
label: 'Object',
name: 'object',
widget: 'object',
i18n: true,
fields: [
{ label: 'String', name: 'string', widget: 'string', i18n: true },
{ label: 'Date', name: 'date', widget: 'datetime', i18n: 'duplicate' },
{ label: 'Boolean', name: 'boolean', widget: 'boolean', i18n: 'duplicate' },
{
label: 'Object',
name: 'object',
widget: 'object',
i18n: true,
field: { label: 'String', name: 'string', widget: 'string', i18n: 'duplicate' },
},
],
},
```
</CodeTabs>
## Folder Collections Path
See [Folder Collections Path](/docs/collection-types#folder-collections-path).
@ -140,176 +289,45 @@ See [Folder Collections Path](/docs/collection-types#folder-collections-path).
Seed [Nested Collections](/docs/collection-types#nested-collections).
## List Widget: Variable Types
Before this feature, the [list widget](/docs/widgets/#list) allowed a set of fields to be repeated, but every list item had the same set of fields available. With variable types, multiple named sets of fields can be defined, which opens the door to highly flexible content authoring (even page building) in Static CMS.
**Note: this feature does not yet support default previews and requires [registering a preview template](/docs/customization#registerpreviewtemplate) in order to show up in the preview pane.**
To use variable types in the list widget, update your field configuration as follows:
1. Instead of defining your list fields under `fields` or `field`, define them under `types`. Similar to `fields`, `types` must be an array of field definition objects.
2. Each field definition under `types` must use the `object` widget (this is the default value for
`widget`).
### Additional list widget options
- `types`: a nested list of object widgets. All widgets must be of type `object`. Every object widget may define different set of fields.
- `typeKey`: the name of the field that will be added to every item in list representing the name of the object widget that item belongs to. Ignored if `types` is not defined. Default is `type`.
- `summary`: allows customization of a collapsed list item object in a similar way to a [collection summary](/docs/configuration-options/?#summary)
### Example Configuration
The example configuration below imagines a scenario where the editor can add two "types" of content,
either a "carousel" or a "spotlight". Each type has a unique name and set of fields.
```yaml
- label: 'Home Section'
name: 'sections'
widget: 'list'
types:
- label: 'Carousel'
name: 'carousel'
widget: object
summary: '{{fields.header}}'
fields:
- { label: Header, name: header, widget: string, default: 'Image Gallery' }
- { label: Template, name: template, widget: string, default: 'carousel.html' }
- label: Images
name: images
widget: list
field: { label: Image, name: image, widget: image }
- label: 'Spotlight'
name: 'spotlight'
widget: object
fields:
- { label: Header, name: header, widget: string, default: 'Spotlight' }
- { label: Template, name: template, widget: string, default: 'spotlight.html' }
- { label: Text, name: text, widget: text, default: 'Hello World' }
```
### Example Output
The output for the list widget will be an array of objects, and each object will have a `type` key
with the name of the type used for the list item. The `type` key name can be customized via the
`typeKey` property in the list configuration.
If the above example configuration were used to create a carousel, a spotlight, and another
carousel, the output could look like this:
```yaml
title: Home
sections:
- type: carousel
header: Image Gallery
template: carousel.html
images:
- images/image01.png
- images/image02.png
- images/image03.png
- type: spotlight
header: Spotlight
template: spotlight.html
text: Hello World
- type: carousel
header: Image Gallery
template: carousel.html
images:
- images/image04.png
- images/image05.png
- images/image06.png
```
## Custom Mount Element
Static CMS always creates its own DOM element for mounting the application, which means it always takes over the entire page, and is generally inflexible if you're trying to do something creative, like injecting it into a shared context.
You can now provide your own element for Static CMS to mount in by setting the target element's ID as `nc-root`. If Static CMS finds an element with this ID during initialization, it will mount within that element instead of creating its own.
## Manual Initialization
Static CMS can now be manually initialized, rather than automatically loading up the moment you import it. The whole point of this at the moment is to inject configuration into Static CMS before it loads, bypassing need for an actual Static CMS `config.yml`. This is important, for example, when creating tight integrations with static site generators.
Manual initialization works by setting `window.CMS_MANUAL_INIT = true` **before importing the CMS**:
```js
// This global flag enables manual initialization.
window.CMS_MANUAL_INIT = true
// Usage with import from npm package
import CMS, { init } from '@staticcms/core'
// Usage with script tag
const { CMS, initCMS: init } = window
/**
* Initialize without passing in config - equivalent to just importing
* Static CMS the old way.
*/
init()
/**
* Optionally pass in a config object. This object will be merged into
* `config.yml` if it exists, and any portion that conflicts with
* `config.yml` will be overwritten. Arrays will be replaced during merge,
* not concatenated.
*
* For example, the code below contains an incomplete config, but using it,
* your `config.yml` can be missing its backend property, allowing you
* to set this property at runtime.
*/
init({
config: {
backend: {
name: 'git-gateway',
},
},
})
/**
* Optionally pass in a complete config object and set a flag
* (`load_config_file: false`) to ignore the `config.yml`.
*
* For example, the code below contains a complete config. The
* `config.yml` will be ignored when setting `load_config_file` to false.
* It is not required if the `config.yml` file is missing to set
* `load_config_file`, but will improve performance and avoid a load error.
*/
init({
config: {
backend: {
name: 'git-gateway',
},
load_config_file: false,
media_folder: "static/images/uploads",
public_folder: "/images/uploads",
collections: [
{ label: "Blog", name: "blog", folder: "_posts/blog", create: true, fields: [
{ label: "Title", name: "title", widget: "string" },
{ label: "Publish Date", name: "date", widget: "datetime" },
{ label: "Featured Image", name: "thumbnail", widget: "image" },
{ label: "Body", name: "body", widget: "markdown" },
]},
],
},
})
// The registry works as expected, and can be used before or after init.
CMS.registerPreviewTemplate(...);
```
## Commit Message Templates
You can customize the templates used by Static CMS to generate commit messages by setting the `commit_messages` option under `backend` in your Static CMS `config.yml`.
You can customize the templates used by Static CMS to generate commit messages by setting the `commit_messages` option under `backend` in your Static CMS `config`.
Template tags wrapped in curly braces will be expanded to include information about the file changed by the commit. For example, `{{path}}` will include the full path to the file changed.
Setting up your Static CMS `config.yml` to recreate the default values would look like this:
Setting up your Static CMS `config` to recreate the default values would look like this:
<CodeTabs>
```yaml
backend:
commit_messages:
create: Create {{collection}} “{{slug}}”
update: Update {{collection}} “{{slug}}”
delete: Delete {{collection}} “{{slug}}”
uploadMedia: Upload “{{path}}”
deleteMedia: Delete “{{path}}”
create: Create {{collection}} "{{slug}}"
update: Update {{collection}} "{{slug}}"
delete: Delete {{collection}} "{{slug}}"
uploadMedia: Upload "{{path}}"
deleteMedia: Delete "{{path}}"
```
```js
backend: {
commit_messages: {
create: 'Create {{collection}} "{{slug}}"',
update: 'Update {{collection}} "{{slug}}"',
delete: 'Delete {{collection}} "{{slug}}"',
uploadMedia: 'Upload "{{path}}"',
deleteMedia: 'Delete "{{path}}"',
},
},
```
</CodeTabs>
Static CMS generates the following commit types:
| Commit type | When is it triggered? | Available template tags |
@ -335,6 +353,7 @@ You can set a limit to as what the maximum file size of a file is that users can
Example config:
<CodeTabs>
```yaml
- label: 'Featured Image'
name: 'thumbnail'
@ -345,24 +364,59 @@ Example config:
max_file_size: 512000 # in bytes, only for default media library
```
```js
{
label: 'Featured Image',
name: 'thumbnail',
widget: 'image',
default: '/uploads/chocolate-dogecoin.jpg',
media_library: {
config: {
max_file_size: 512000 // in bytes, only for default media library
},
},
},
```
</CodeTabs>
## Summary string template transformations
You can apply transformations on fields in a summary string template using filter notation syntax.
Example config:
<CodeTabs>
```yaml
collections:
- name: 'posts'
label: 'Posts'
folder: '_posts'
summary: "{{title | upper}} - {{date | date('YYYY-MM-DD')}} {{body | truncate(20, '***')}}"
summary: "{{title | upper}} - {{date | date('YYYY-MM-DD')}} - {{body | truncate(20, '***')}}"
fields:
- { label: 'Title', name: 'title', widget: 'string' }
- { label: 'Publish Date', name: 'date', widget: 'datetime' }
- { label: 'Body', name: 'body', widget: 'markdown' }
```
```js
collections: [
{
name: 'posts',
label: 'Posts',
folder: '_posts',
summary: "{{title | upper}} - {{date | date('YYYY-MM-DD')}} - {{body | truncate(20, '***')}}",
fields: [
{ label: 'Title', name: 'title', widget: 'string' },
{ label: 'Publish Date', name: 'date', widget: 'datetime' },
{ label: 'Body', name: 'body', widget: 'markdown' },
],
},
],
```
</CodeTabs>
The above config will transform the title field to uppercase and format the date field using `YYYY-MM-DD` format.
Available transformations are `upper`, `lower`, `date('<format>')`, `default('defaultValue')`, `ternary('valueForTrue','valueForFalse')` and `truncate(<number>)`/`truncate(<number>, '<string>')`
@ -396,6 +450,7 @@ When linking to `/admin/#/collections/posts/new` you can pass URL parameters to
For example given the configuration:
<CodeTabs>
```yaml
collections:
- name: posts
@ -418,6 +473,43 @@ collections:
widget: markdown
```
```js
collections: [
{
name: 'posts',
label: 'Posts',
folder: 'content/posts',
create: true,
fields: [
{
label: 'Title',
name: 'title',
widget: 'string'
},
{
label: 'Object',
name: 'object',
widget: 'object',
fields: [
{
label: 'Title',
name: 'title',
widget: 'string'
}
],
},
{
label: 'body',
name: 'body',
widget: 'markdown'
},
],
},
],
```
</CodeTabs>
clicking the following link: `/#/collections/posts/new?title=first&object.title=second&body=%23%20content`
will open the editor for a new post with the `title` field populated with `first`, the nested `object.title` field

View File

@ -13,10 +13,19 @@ For repositories stored on Bitbucket, the `bitbucket` backend allows CMS users t
To enable Bitbucket authentication it:
1. Follow the authentication provider setup steps in the [Netlify docs](https://www.netlify.com/docs/authentication-providers/#using-an-authentication-provider).
2. Add the following lines to your Static CMS `config.yml` file:
2. Add the following lines to your Static CMS `config` file:
<CodeTabs>
```yaml
backend:
name: bitbucket
repo: owner-name/repo-name # Path to your Bitbucket repository
```
```js
backend: {
name: 'bitbucket',
repo: 'owner-name/repo-name', // Path to your Bitbucket repository
},
```
</CodeTabs>

View File

@ -20,6 +20,7 @@ You can [sign up for Cloudinary](https://cloudinary.com/users/register/free) for
To use the Cloudinary media library within Static CMS, you'll need to update your Static CMS configuration file with the information from your Cloudinary account:
<CodeTabs>
```yaml
media_library:
name: cloudinary
@ -28,11 +29,23 @@ media_library:
api_key: your_api_key
```
```js
media_library: {
name: 'cloudinary',
config: {
cloud_name: 'your_cloud_name',
api_key: 'your_api_key'
},
},
```
</CodeTabs>
**Note:** The user must be logged in to the Cloudinary account connected to the `api_key` used in your Static CMS configuration.
### Security Considerations
Although this setup exposes the `cloud_name` and `api_key` publicly via the `/admin/config.yml` endpoint, this information is not sensitive. Any integration of the Cloudinary media library requires this information to be exposed publicly. To use this library or use the restricted Cloudinary API endpoints, the user must have access to the Cloudinary account login details or the `api_secret` associated with the `cloud_name` and `api_key`.
Although this setup exposes the `cloud_name` and `api_key` publicly via the `config` endpoint, this information is not sensitive. Any integration of the Cloudinary media library requires this information to be exposed publicly. To use this library or use the restricted Cloudinary API endpoints, the user must have access to the Cloudinary account login details or the `api_secret` associated with the `cloud_name` and `api_key`.
## Static CMS configuration options
@ -75,6 +88,8 @@ Global configuration, which is meant to affect the Cloudinary widget at all time
as seen below, under the primary `media_library` property. Settings applied here will affect every
instance of the Cloudinary widget.
<CodeTabs>
```yaml
# global
media_library:
@ -88,12 +103,35 @@ media_library:
crop: scale
```
```js
// global
media_library: {
name: 'cloudinary',
output_filename_only: false,
config: {
default_transformations: [
[
{
fetch_format: 'auto',
width: 160,
quality: 'auto',
crop: 'scale'
}
],
],
},
},
```
</CodeTabs>
#### Field configuration
Configuration can also be provided for individual fields that use the media library. The structure
is very similar to the global configuration, except the settings are added to an individual `field`.
For example:
<CodeTabs>
```yaml
# field
fields: # The fields each document in this collection have
@ -112,23 +150,80 @@ fields: # The fields each document in this collection have
effect: grayscale
```
```js
// field
fields: [
{
label: 'Cover Image',
name: 'image',
widget: 'image',
required: false,
tagtitle: '',
media_library: {
config: {
default_transformations: [
{
fetch_format: 'auto',
width: 300,
quality: 'auto',
crop: 'fill',
effect: 'grayscale',
},
],
},
},
},
],
```
</CodeTabs>
## Inserting Cloudinary URL in page templates
If you prefer to provide direction so that images are transformed in a specific way, or dynamically retrieve images based on viewport size, you can do so by providing your own base Cloudinary URL and only storing the asset filenames in your content:
- Either globally or for specific fields, configure the Cloudinary extension to only output the asset filename
**Global**
<CodeTabs>
```yaml
# global
media_library:
name: cloudinary
output_filename_only: true
```
```js
// global
media_library: {
name: 'cloudinary',
output_filename_only: true,
},
```
</CodeTabs>
**Field**
<CodeTabs>
```yaml
# field
media_library:
name: cloudinary
output_filename_only: true
```
```js
// field
media_library: {
name: 'cloudinary',
output_filename_only: true,
},
```
</CodeTabs>
- Provide a dynamic URL in the site template
```handlebars

View File

@ -6,58 +6,66 @@ weight: 9
`collections` accepts a list of collection objects, each with the following options
| Name | Type | Default | Description |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| name | string | | Unique identifier for the collection, used as the key when referenced in other contexts (like the [relation widget](/docs/widgets/#relation)) |
| identifier_field | string | `'title'` | _Optional_. See [identifier_field](#identifier_field) below |
| label | string | `name` | _Optional_. Label for the collection in the editor UI |
| label_singular | string | `label` | _Optional_. singular label for certain elements in the editor |
| icon | string | | Unique name of icon to use in main menu. See [Custom Icons](/docs/custom-icons) |
| description | string | | _Optional_. Text displayed below the label when viewing a collection |
| files or folder | [Collection Files](/docs/collection-types#file-collections)<br />\| [Collection Folder](/docs/collection-types#folder-collections) | | **Requires one of these**: Specifies the collection type and location; details in [Collection Types](/docs/collection-types) |
| filter | string | | _Optional_. Filter for [Folder Collections](/docs/collection-types#folder-collections) |
| create | string | `false` | **For [Folder Collections](/docs/collection-types#folder-collections) only**<br />`true` - Allows users to create new items in the collection |
| hide | string | `false` | `true` hides a collection in the CMS UI. Useful when using the relation widget to hide referenced collections |
| delete | string | `true` | `false` prevents users from deleting items in a collection |
| extension | string | | See [extension](#extension-and-format) below |
| format | string | | See [format](#extension-and-format) below |
| frontmatter_delimiter | string | | See [frontmatter_delimiter](#frontmatter_delimiter) below |
| slug | string | | See [slug](#slug) below |
| fields (required) | string | | See [fields](#fields) below |
| editor | string | | See [editor](#editor) below |
| summary | string | | See [summary](#summary) below |
| sortable_fields | string | | See [sortable_fields](#sortable_fields) below |
| view_filters | string | | See [view_filters](#view_filters) below |
| view_groups | string | | See [view_groups](#view_groups) below |
| Name | Type | Default | Description |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | string | | Unique identifier for the collection, used as the key when referenced in other contexts (like the [relation widget](/docs/widgets/#relation)) |
| identifier_field | string | `'title'` | _Optional_. See [identifier_field](#identifier_field) below |
| label | string | `name` | _Optional_. Label for the collection in the editor UI |
| label_singular | string | `label` | _Optional_. Singular label for certain elements in the editor |
| icon | string | | _Optional_. Unique name of icon to use in main menu. See [Custom Icons](/docs/custom-icons) |
| description | string | | _Optional_. Text displayed below the label when viewing a collection |
| files or folder | [Collection Files](/docs/collection-types#file-collections)<br />\| [Collection Folder](/docs/collection-types#folder-collections) | | **Requires one of these**: Specifies the collection type and location; details in [Collection Types](/docs/collection-types) |
| filter | string | | _Optional_. Filter for [Folder Collections](/docs/collection-types#folder-collections) |
| create | string | `false` | _Optional_. **For [Folder Collections](/docs/collection-types#folder-collections) only**<br />`true` - Allows users to create new items in the collection |
| hide | string | `false` | _Optional_. `true` hides a collection in the CMS UI. Useful when using the relation widget to hide referenced collections |
| delete | string | `true` | _Optional_. `false` prevents users from deleting items in a collection |
| extension | string | | _Optional_. See [extension](#extension-and-format) below |
| format | string | | _Optional_. See [format](#extension-and-format) below |
| frontmatter_delimiter | string | | _Optional_. See [frontmatter_delimiter](#frontmatter_delimiter) below |
| slug | string | | _Optional_. See [slug](#slug) below |
| fields (required) | string | | _Optional_. See [fields](#fields) below. Ignored if [Files Collection](/docs/collection-types#file-collections) |
| editor | string | | _Optional_. See [editor](#editor) below |
| summary | string | | _Optional_. See [summary](#summary) below |
| sortable_fields | string | | _Optional_. See [sortable_fields](#sortable_fields) below |
| view_filters | string | | _Optional_. See [view_filters](#view_filters) below |
| view_groups | string | | _Optional_. See [view_groups](#view_groups) below |
## `identifier_field`
Static CMS expects every entry to provide a field named `"title"` that serves as an identifier for the entry. The identifier field serves as an entry's title when viewing a list of entries, and is used in [slug](#slug) creation. If you would like to use a field other than `"title"` as the identifier, you can set `identifier_field` to the name of the other field.
### Example
<CodeTabs>
```yaml
collections:
- name: posts
identifier_field: name
```
```js
collections: [
{
name: 'posts',
identifier_field: 'name',
},
],
```
</CodeTabs>
## `extension` and `format`
These settings determine how collection files are parsed and saved. Both are optional—Static CMS will attempt to infer your settings based on existing items in the collection. If your collection is empty, or you'd like more control, you can set these fields explicitly.
`extension` determines the file extension searched for when finding existing entries in a folder collection and it determines the file extension used to save new collection items. It accepts the following values: `yml`, `yaml`, `toml`, `json`, `md`, `markdown`, `html`.
`extension` determines the file extension searched for when finding existing entries in a folder collection and it determines the file extension used to save new collection items. It accepts the following values: `yml`, `yaml`, `json`, `md`, `markdown`, `html`.
You may also specify a custom `extension` not included in the list above, as long as the collection files can be parsed and saved in one of the supported formats below.
`format` determines how collection files are parsed and saved. It will be inferred if the `extension` field or existing collection file extensions match one of the supported extensions above. It accepts the following values:
- `yml` or `yaml`: parses and saves files as YAML-formatted data files; saves with `yml` extension by default
- `toml`: parses and saves files as TOML-formatted data files; saves with `toml` extension by default
- `json`: parses and saves files as JSON-formatted data files; saves with `json` extension by default
- `frontmatter`: parses files and saves files with data frontmatter followed by an unparsed body text (edited using a `body` field); saves with `md` extension by default; default for collections that can't be inferred. Collections with `frontmatter` format (either inferred or explicitly set) can parse files with frontmatter in YAML, TOML, or JSON format. However, they will be saved with YAML frontmatter.
- `frontmatter`: parses files and saves files with data frontmatter followed by an unparsed body text (edited using a `body` field); saves with `md` extension by default; default for collections that can't be inferred. Collections with `frontmatter` format (either inferred or explicitly set) can parse files with frontmatter in YAML or JSON format. However, they will be saved with YAML frontmatter.
- `yaml-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved only as YAML, followed by unparsed body text. The default delimiter for this option is `---`.
- `toml-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved only as TOML, followed by unparsed body text. The default delimiter for this option is `+++`.
- `json-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved as JSON, followed by unparsed body text. The default delimiter for this option is `{` `}`.
## `frontmatter_delimiter`
@ -70,7 +78,7 @@ For folder collections where users can create new items, the `slug` option speci
The slug template can also reference a field value by name, eg. `{{title}}`. If a field name conflicts with a built in template tag name - for example, if you have a field named `slug`, and would like to reference that field via `{{slug}}`, you can do so by adding the explicit `fields.` prefix, eg. `{{fields.slug}}`.
### Available Template Tags
**Available Template Tags**
- Any field can be referenced by wrapping the field name in double curly braces, eg. `{{author}}`
- `{{slug}}`: a url-safe version of the `title` field (or identifier field) for the file
@ -85,32 +93,54 @@ The slug template can also reference a field value by name, eg. `{{title}}`. If
#### Basic Example
<CodeTabs>
```yaml
slug: '{{year}}-{{month}}-{{day}}_{{slug}}'
```
```js
slug: '{{year}}-{{month}}-{{day}}_{{slug}}',
```
</CodeTabs>
#### Field Names
<CodeTabs>
```yaml
slug: '{{year}}-{{month}}-{{day}}_{{title}}_{{some_other_field}}'
```
```js
slug: '{{year}}-{{month}}-{{day}}_{{title}}_{{some_other_field}}',
```
</CodeTabs>
#### Field Name That Conflicts With Template Tag
<CodeTabs>
```yaml
slug: '{{year}}-{{month}}-{{day}}_{{fields.slug}}'
```
```js
slug: '{{year}}-{{month}}-{{day}}_{{fields.slug}}',
```
</CodeTabs>
## `fields`
The `fields` option maps editor UI widgets to field-value pairs in the saved file. The order of the fields in your Static CMS `config.yml` file determines their order in the editor UI and in the saved file.
_Ignored if [Files Collection](/docs/collection-types#file-collections)_
The `fields` option maps editor UI widgets to field-value pairs in the saved file. The order of the fields in your Static CMS `config` file determines their order in the editor UI and in the saved file.
`fields` accepts a list of widgets. See [widgets](/docs/widgets) for more details.
In files with frontmatter, one field should be named `body`. This special field represents the section of the document (usually markdown) that comes after the frontmatter.
### Example
<CodeTabs>
```yaml
fields:
- label: "Title"
@ -119,10 +149,20 @@ fields:
pattern: ['.{20,}', "Must have at least 20 characters"]
- {label: "Layout", name: "layout", widget: "hidden", default: "blog"}
- {label: "Featured Image", name: "thumbnail", widget: "image", required: false}
- {label: "Body", name: "body", widget: "markdown"}
comment: 'This is a multiline\ncomment'
- {label: "Body", name: "body", widget: "markdown", comment: 'This is a multiline\ncomment' }
```
```js
fields: [
{ label: 'Title', name: 'title', widget: 'string', pattern: ['.{20,}', 'Must have at least 20 characters'] },
{ label: 'Layout', name: 'layout', widget: 'hidden', default: 'blog' },
{ label: 'Featured Image', name: 'thumbnail', widget: 'image', required: false },
{ label: 'Body', name: 'body', widget: 'markdown', comment: 'This is a multiline\\ncomment' },
],
```
</CodeTabs>
## `editor`
This setting changes options for the editor view of a collection or a file inside a files collection. It has the following options:
@ -132,21 +172,29 @@ This setting changes options for the editor view of a collection or a file insid
| preview | boolean | `true` | Set to `false` to disable the preview pane for this collection or file |
| frame | boolean | `true` | <ul><li>`true` - Previews render in a frame</li><li>`false` - Previews render directly in your app</li></ul> |
### Example
<CodeTabs>
```yaml
editor:
preview: false
frame: false
```
```js
editor: {
preview: false,
frame: false,
},
```
</CodeTabs>
**Note**: Setting this as a top level configuration will set the default for all collections
## `summary`
This setting allows the customization of the collection list view. Similar to the `slug` field, a string with templates can be used to include values of different fields, e.g. `{{title}}`. This option over-rides the default of `title` field and `identifier_field`.
### Available Template Tags
**Available Template Tags**
Template tags are the same as those for [slug](#slug), with the following additions:
@ -156,12 +204,17 @@ Template tags are the same as those for [slug](#slug), with the following additi
- `{{commit_date}}` The file commit date on supported backends (git based backends).
- `{{commit_author}}` The file author date on supported backends (git based backends).
### Example
<CodeTabs>
```yaml
summary: 'Version: {{version}} - {{title}}'
```
```js
summary: 'Version: {{version}} - {{title}}',
```
</CodeTabs>
## `sortable_fields`
An optional list of sort fields to show in the UI.
@ -170,21 +223,26 @@ Defaults to inferring `title`, `date`, `author` and `description` fields and wil
When `author` field can't be inferred commit author will be used.
### Example
<CodeTabs>
```yaml
# use dot notation for nested fields
sortable_fields: ['commit_date', 'title', 'commit_author', 'language.en']
```
### `view_filters`
```js
// use dot notation for nested fields
sortable_fields: ['commit_date', 'title', 'commit_author', 'language.en'],
```
</CodeTabs>
## `view_filters`
An optional list of predefined view filters to show in the UI.
Defaults to an empty list.
### Example
<CodeTabs>
```yaml
view_filters:
- label: "Alice's and Bob's Posts"
@ -198,14 +256,35 @@ view_filters:
pattern: true
```
```js
view_filters: [
{
label: "Alice's and Bob's Posts",
field: 'author',
pattern: 'Alice|Bob',
},
{
label: 'Posts published in 2020',
field: 'date',
pattern: '2020',
},
{
label: 'Drafts',
field: 'draft',
pattern: true,
},
],
```
</CodeTabs>
## `view_groups`
An optional list of predefined view groups to show in the UI.
Defaults to an empty list.
### Example
<CodeTabs>
```yaml
view_groups:
- label: Year
@ -215,3 +294,19 @@ view_groups:
- label: Drafts
field: draft
```
```js
view_groups: [
{
label: 'Year',
field: 'date',
pattern: '\\d{4}',
},
{
label: 'Drafts',
field: 'draft',
},
],
```
</CodeTabs>

View File

@ -4,7 +4,7 @@ title: Collection Types
weight: 10
---
All editable content types are defined in the `collections` field of your `config.yml` file, and display in the left sidebar of the Content page of the editor UI.
All editable content types are defined in the `collections` field of your `config` file, and display in the left sidebar of the Content page of the editor UI.
Collections come in two main types: `folder` and `files`.
@ -20,6 +20,7 @@ Unlike file collections, folder collections have the option to allow editors to
#### Basic
<CodeTabs>
```yaml
collections:
- name: blog
@ -38,11 +39,31 @@ collections:
widget: image
- name: body
label: Body
widget: 'markdown
widget: 'markdown'
```
```js
collections: [
{
name: 'blog',
label: 'Blog',
folder: '_posts/blog',
create: true,
fields: [
{ name: 'title', label: 'Title', widget: 'string' },
{ name: 'date', label: 'Publish Date', widget: 'datetime' },
{ name: 'thumbnail', label: 'Featured Image', widget: 'image' },
{ name: 'body', label: 'Body', widget: 'markdown' },
],
},
],
```
</CodeTabs>
#### With Identifier Field
<CodeTabs>
```yaml
- name: 'blog'
label: 'Blog'
@ -64,6 +85,24 @@ collections:
widget: markdown
```
```js
{
name: 'blog',
label: 'Blog',
folder: '_posts/blog',
create: true,
identifier_field: 'name',
fields: [
{ name: 'name', label: 'Name', widget: 'string' },
{ name: 'date', label: 'Publish Date', widget: 'datetime' },
{ name: 'thumbnail', label: 'Featured Image', widget: 'image' },
{ name: 'body', label: 'Body', widget: 'markdown' },
],
},
```
</CodeTabs>
### Filtered folder collections
The entries for any folder collection can be filtered based on the value of a single field. By filtering a folder into different collections, you can manage files with different fields, options, extensions, etc. in the same folder.
@ -75,6 +114,7 @@ The `filter` option requires two fields:
The example below creates two collections in the same folder, filtered by the `language` field. The first collection includes posts with `language: en`, and the second, with `language: es`.
<CodeTabs>
```yaml
collections:
- name: 'english_posts'
@ -115,6 +155,37 @@ collections:
widget: markdown
```
```js
collections: [
{
name: 'english_posts',
label: 'Blog in English',
folder: '_posts',
create: true,
filter: { field: 'language', value: 'en' },
fields: [
{ name: 'language', label: 'Language', widget: 'select', options: ['en', 'es'] },
{ name: 'title', label: 'Title', widget: 'string' },
{ name: 'body', label: 'Content', widget: 'markdown' },
],
},
{
name: 'spanish_posts',
label: 'Blog en Español',
folder: '_posts',
create: true,
filter: { field: 'language', value: 'es' },
fields: [
{ name: 'language', label: 'Lenguaje', widget: 'select', options: ['en', 'es'] },
{ name: 'title', label: 'Titulo', widget: 'string' },
{ name: 'body', label: 'Contenido', widget: 'markdown' },
],
},
],
```
</CodeTabs>
### Folder Collections Path <img src="https://img.shields.io/badge/-Beta%20Feature-blue" alt="Beta Feature. Use at your own risk" title="Beta Feature. Use at your own risk" />
By default the CMS stores folder collection content under the folder specified in the collection setting.
@ -133,11 +204,19 @@ When using the global `media_folder` directory any entry field that points to a
For example configuring:
<CodeTabs>
```yaml
media_folder: static/media
public_folder: /media
```
```js
media_folder: 'static/media',
public_folder: '/media'
```
</CodeTabs>
And saving an entry with an image named `image.png` will result in the image being saved under `static/media/image.png` and relevant entry fields populated with the value of `/media/image.png`.
Some static site generators (e.g. Gatsby) work best when using relative image paths.
@ -146,6 +225,7 @@ This can now be achieved using a per collection `media_folder` configuration whi
For example, the following configuration will result in media files being saved in the same directory as the entry, and the image field being populated with the relative path to the image.
<CodeTabs>
```yaml
media_folder: static/media
public_folder: /media
@ -166,6 +246,28 @@ collections:
widget: 'image'
```
```js
media_folder: 'static/media',
public_folder: '/media',
collections: [
{
name: 'posts',
label: 'Posts',
label_singular: 'Post',
folder: 'content/posts',
path: '{{slug}}/index',
media_folder: '',
public_folder: '',
fields: [
{ label: 'Title', name: 'title', widget: 'string' },
{ label: 'Cover Image', name: 'image', widget: 'image' },
],
},
],
```
</CodeTabs>
More specifically, saving an entry with a title of `example post` with an image named `image.png` will result in a directory structure of:
```bash
@ -184,11 +286,11 @@ And for the image field being populated with a value of `image.png`.
Supports all of the [`slug` templates](/docs/configuration-options#slug) and:
* `{{dirname}}` The path to the file's parent directory, relative to the collection's `folder`.
* `{{filename}}` The file name without the extension part.
* `{{extension}}` The file extension.
* `{{media_folder}}` The global `media_folder`.
* `{{public_folder}}` The global `public_folder`.
- `{{dirname}}` The path to the file's parent directory, relative to the collection's `folder`.
- `{{filename}}` The file name without the extension part.
- `{{extension}}` The file extension.
- `{{media_folder}}` The global `media_folder`.
- `{{public_folder}}` The global `public_folder`.
### Nested Collections <img src="https://img.shields.io/badge/-Beta%20Feature-blue" alt="Beta Feature. Use at your own risk" title="Beta Feature. Use at your own risk" />
@ -202,8 +304,7 @@ When configuring a `files` collection, configure each file in the collection sep
**Note:** Files listed in a file collection must already exist in the hosted repository branch set in your Static CMS [backend configuration](/docs/backends-overview). Files must also have a valid value for the file type. For example, an empty file works as valid YAML, but a JSON file must have a non-empty value to be valid, such as an empty object.
### Example
<CodeTabs>
```yaml
collections:
- name: pages
@ -253,3 +354,53 @@ collections:
label: Address
widget: string
```
```js
collections: [
{
name: 'pages',
label: 'Pages',
files: [
{
name: 'about',
label: 'About Page',
file: 'site/content/about.yml',
fields: [
{ name: 'title', label: 'Title', widget: 'string' },
{ name: 'intro', label: 'Intro', widget: 'markdown' },
{
name: 'team',
label: 'Team',
widget: 'list',
fields: [
{ name: 'name', label: 'Name', widget: 'string' },
{ name: 'position', label: 'Position', widget: 'string' },
{ name: 'photo', label: 'Photo', widget: 'image' },
],
},
],
},
{
name: 'locations',
label: 'Locations Page',
file: 'site/content/locations.yml',
fields: [
{ name: 'title', label: 'Title', widget: 'string' },
{ name: 'intro', label: 'Intro', widget: 'markdown' },
{
name: 'locations',
label: 'Locations',
widget: 'list',
fields: [
{ name: 'name', label: 'Name', widget: 'string' },
{ name: 'address', label: 'Address', widget: 'string' },
],
},
],
},
],
},
],
```
</CodeTabs>

View File

@ -1,7 +1,7 @@
---
group: Intro
title: Configuration Options
weight: 180
weight: 10
---
All configuration options for Static CMS are specified in a `config.yml` file, in the folder where you access the editor UI (usually in the `/admin` folder).
@ -10,20 +10,22 @@ Alternatively, you can specify a custom config file using a link tag:
```html
<!-- Note the "type" and "rel" attribute values, which are required. -->
<link href="path/to/config.yml" type="text/yaml" rel="cms-config-url">
<link href="path/to/config.yml" type="text/yaml" rel="cms-config-url" />
```
If you prefer, you can use a javascript file (`admin/config.js`) instead of a yaml file. Simply import the javascript config and pass it into your `CMS.init({ config })` call.
To see working configuration examples, you can [start from a template](/docs/start-with-a-template) or check out the [CMS demo site](https://static-cms-demo.netlify.app). (No login required: click the login button and the CMS will open.) You can refer to the demo [configuration code](https://github.com/StaticJsCMS/static-cms/blob/main/core/dev-test/config.yml) to see how each option was configured.
You can find details about all configuration options below. Note that [YAML syntax](https://en.wikipedia.org/wiki/YAML#Basic_components) allows lists and objects to be written in block or inline style, and the code samples below include a mix of both.
## Backend
*This setting is required.*
_This setting is required._
The `backend` option specifies how to access the content for your site, including authentication. Full details and code samples can be found in [Backends](/docs/backends-overview).
**Note**: no matter where you access Static CMS — whether running locally, in a staging environment, or in your published site — it will always fetch and commit files in your hosted repository (for example, on GitHub), on the branch you configured in your Static CMS config.yml file. This means that content fetched in the admin UI will match the content in the repository, which may be different from your locally running site. It also means that content saved using the admin UI will save directly to the hosted repository, even if you're running the UI locally or in staging. If you want to have your local CMS write to a local repository, [try the local_backend setting](/docs/local-backend).
**Note**: no matter where you access Static CMS — whether running locally, in a staging environment, or in your published site — it will always fetch and commit files in your hosted repository (for example, on GitHub), on the branch you configured in your Static CMS config file. This means that content fetched in the admin UI will match the content in the repository, which may be different from your locally running site. It also means that content saved using the admin UI will save directly to the hosted repository, even if you're running the UI locally or in staging. If you want to have your local CMS write to a local repository, [try the local_backend setting](/docs/local-backend).
## Media and Public Folders
@ -31,22 +33,36 @@ Static CMS users can upload files to your repository using the Media Gallery. Th
### Media Folder
*This setting is required.*
_This setting is required._
The `media_folder` option specifies the folder path where uploaded files should be saved, relative to the base of the repo.
<CodeTabs>
```yaml
media_folder: "static/images/uploads"
```
```js
media_folder: 'static/images/uploads',
```
</CodeTabs>
### Public Folder
The `public_folder` option specifies the folder path where the files uploaded by the media library will be accessed, relative to the base of the built site. For fields controlled by \[file] or \[image] widgets, the value of the field is generated by prepending this path to the filename of the selected file. Defaults to the value of `media_folder`, with an opening `/` if one is not already included.
<CodeTabs>
```yaml
public_folder: "/images/uploads"
```
```js
public_folder: '/images/uploads',
```
</CodeTabs>
Based on the settings above, if a user used an image widget field called `avatar` to upload and select an image called `philosoraptor.png`, the image would be saved to the repository at `/static/images/uploads/philosoraptor.png`, and the `avatar` field for the file would be set to `/images/uploads/philosoraptor.png`.
This setting can be set to an absolute URL e.g. `https://netlify.com/media` should you wish, however in general this is not advisable as content should have relative paths to other content.
@ -57,6 +73,7 @@ Media library integrations are configured via the `media_library` property, and
**Example:**
<CodeTabs>
```yaml
media_library:
name: uploadcare
@ -64,16 +81,34 @@ media_library:
publicKey: demopublickey
```
```js
media_library: {
name: 'uploadcare',
config: {
publicKey: 'demopublickey'
}
},
```
</CodeTabs>
## Site URL
The `site_url` setting should provide a URL to your published site. May be used by the CMS for various functionality. Used together with a collection's `preview_path` to create links to live content.
**Example:**
<CodeTabs>
```yaml
site_url: https://your-site.com
```
```js
site_url: 'https://your-site.com',
```
</CodeTabs>
## Display URL
When the `display_url` setting is specified, the CMS UI will include a link in the fixed area at the top of the page, allowing content authors to easily return to your main site. The text of the link consists of the URL with the protocol portion (e.g., `https://your-site.com`).
@ -82,20 +117,34 @@ Defaults to `site_url`.
**Example:**
<CodeTabs>
```yaml
display_url: https://your-site.com
```
```js
display_url: 'https://your-site.com',
```
</CodeTabs>
## Custom Logo
When the `logo_url` setting is specified, the CMS UI will change the logo displayed at the top of the login page, allowing you to brand the CMS with your own logo. `logo_url` is assumed to be a URL to an image file.
**Example:**
<CodeTabs>
```yaml
logo_url: https://your-site.com/images/logo.svg
```
```js
logo_url: 'https://your-site.com/images/logo.svg',
```
</CodeTabs>
## Locale
The CMS locale. Defaults to `en`.
@ -104,12 +153,19 @@ Other languages than English must be registered manually.
**Example**
In your `config.yml`:
In your `config`:
<CodeTabs>
```yaml
locale: 'de'
```
```js
locale: 'de',
```
</CodeTabs>
And in your custom JavaScript code:
```js
@ -120,7 +176,7 @@ CMS.registerLocale('de', de);
When a translation for the selected locale is missing the English one will be used.
> All locales are registered by default (so you only need to update your `config.yml`).
> All locales are registered by default (so you only need to update your `config`).
## Search
@ -130,10 +186,17 @@ Defaults to `true`
**Example:**
<CodeTabs>
```yaml
search: false
```
```js
search: false,
```
</CodeTabs>
## Slug Type
The `slug` option allows you to change how filenames for entries are created and sanitized. It also applies to filenames of media inserted via the default media library.\
@ -141,15 +204,17 @@ For modifying the actual data in a slug, see the per-collection option below.
`slug` accepts multiple options:
* `encoding`
- `encoding`
* `unicode` (default): Sanitize filenames (slugs) according to [RFC3987](https://tools.ietf.org/html/rfc3987) and the [WHATWG URL spec](https://url.spec.whatwg.org/). This spec allows non-ASCII (or non-Latin) characters to exist in URLs.
* `ascii`: Sanitize filenames (slugs) according to [RFC3986](https://tools.ietf.org/html/rfc3986). The only allowed characters are (0-9, a-z, A-Z, `_`, `-`, `~`).
* `clean_accents`: Set to `true` to remove diacritics from slug characters before sanitizing. This is often helpful when using `ascii` encoding.
* `sanitize_replacement`: The replacement string used to substitute unsafe characters, defaults to `-`.
- `unicode` (default): Sanitize filenames (slugs) according to [RFC3987](https://tools.ietf.org/html/rfc3987) and the [WHATWG URL spec](https://url.spec.whatwg.org/). This spec allows non-ASCII (or non-Latin) characters to exist in URLs.
- `ascii`: Sanitize filenames (slugs) according to [RFC3986](https://tools.ietf.org/html/rfc3986). The only allowed characters are (0-9, a-z, A-Z, `_`, `-`, `~`).
- `clean_accents`: Set to `true` to remove diacritics from slug characters before sanitizing. This is often helpful when using `ascii` encoding.
- `sanitize_replacement`: The replacement string used to substitute unsafe characters, defaults to `-`.
**Example**
<CodeTabs>
```yaml
slug:
encoding: "ascii"
@ -157,10 +222,20 @@ slug:
sanitize_replacement: "_"
```
```js
slug: {
encoding: 'ascii',
clean_accents: true,
sanitize_replacement: '_'
},
```
</CodeTabs>
## Collections
*This setting is required.*
_This setting is required._
The `collections` setting is the heart of your Static CMS configuration, as it determines how content types and editor fields in the UI generate files and content in your repository. Each collection you configure displays in the left sidebar of the Content page of the editor UI, in the order they are entered into your Static CMS `config.yml` file.
The `collections` setting is the heart of your Static CMS configuration, as it determines how content types and editor fields in the UI generate files and content in your repository. Each collection you configure displays in the left sidebar of the Content page of the editor UI, in the order they are entered into your Static CMS `config` file.
`collections` accepts a list of collection objects. See [Collections](/docs/collection-overview) for details.
`collections` accepts a list of collection objects. See [Collections](/docs/collection-overview) for details.

View File

@ -25,3 +25,29 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
cmsApp.registerIcon('house', <FontAwesomeIcon icon={faHouse} size="lg" />);
```
## Usage
### Collection
<CodeTabs>
```yaml
collections:
- name: homepage
icon: house
```
```js
collections: [
{
name: 'homepage',
icon: 'house'
},
],
```
</CodeTabs>
### Additional Links
See [Additional Links](/docs/additional-links#examples) for details.

View File

@ -163,9 +163,10 @@ Register widget takes an optional object of options. These options include:
</script>
```
`admin/config.yml`
`admin/config.yml` (or `admin/config.js`)
```yml
<CodeTabs>
```yaml
collections:
- name: posts
label: Posts
@ -180,6 +181,30 @@ collections:
separator: __
```
```js
collections: [
{
name: 'posts',
label: 'Posts',
folder: 'content/posts',
fields: [
{
name: 'title'
label: 'Title'
widget: 'string'
},
{
name: 'categories'
label: 'Categories'
widget: 'categories'
separator: '__'
}
]
}
]
```
</CodeTabs>
## Advanced field validation
All widget fields, including those for built-in widgets, [include basic validation](/docs/widgets/#common-widget-options) capability using the `required` and `pattern` options.

View File

@ -129,7 +129,7 @@ Netlify's Identity and Git Gateway services allow you to manage CMS admin users
## Start publishing
It's time to create your first blog post. Login to your site's `/admin/` page and create a new post by clicking New Blog. Add a title, a date and some text. When you click Publish, a new commit will be created in your GitHub repo with this format `Create Blog “year-month-date-title”`.
It's time to create your first blog post. Login to your site's `/admin/` page and create a new post by clicking New Blog. Add a title, a date and some text. When you click Publish, a new commit will be created in your GitHub repo with this format `Create Blog "year-month-date-title"`.
Then Netlify will detect that there was a commit in your repo, and will start rebuilding your project. When your project is deployed you'll be able to see the post you created.

View File

@ -15,13 +15,22 @@ The [Netlify Identity](https://www.netlify.com/docs/identity/) service can handl
To use it in your own project stored on GitHub or GitLab, follow these steps:
1. Head over to the [Netlify Identity docs](https://www.netlify.com/docs/identity) and follow the steps to get started.
2. Add the following lines to your Static CMS `config.yml` file:
2. Add the following lines to your Static CMS `config` file:
<CodeTabs>
```yaml
backend:
name: git-gateway
```
```js
backend: {
name: 'git-gateway',
},
```
</CodeTabs>
## Reconnect after Changing Repository Permissions
If you change ownership on your repository, or convert a repository from public to private, you may need to reconnect Git Gateway with proper permissions. Find further instructions in the [Netlify Git Gateway docs](https://www.netlify.com/docs/git-gateway/#reconnect-after-changing-repository-permissions).
@ -30,4 +39,4 @@ If you change ownership on your repository, or convert a repository from public
You can use [Git Gateway](https://github.com/netlify/git-gateway) without Netlify by setting up your own Git Gateway server and connecting it with your own instance of [GoTrue](https://www.gotrueapi.org) (the open source microservice that powers Netlify Identity), or with any other identity service that can issue JSON Web Tokens (JWT).
To configure in Static CMS, use the same `backend` settings in your Static CMS `config.yml` file as described in Step 2 of the [Git Gateway with Netlify Identity](#git-gateway-with-netlify-identity) instructions above.
To configure in Static CMS, use the same `backend` settings in your Static CMS `config` file as described in Step 2 of the [Git Gateway with Netlify Identity](#git-gateway-with-netlify-identity) instructions above.

View File

@ -15,8 +15,9 @@ Because Github requires a server for authentication, Netlify facilitates basic G
To enable basic GitHub authentication:
1. Follow the authentication provider setup steps in the [Netlify docs](https://www.netlify.com/docs/authentication-providers/#using-an-authentication-provider).
2. Add the following lines to your Static CMS `config.yml` file:
2. Add the following lines to your Static CMS `config` file:
<CodeTabs>
```yaml
backend:
name: github
@ -25,6 +26,17 @@ backend:
# branch: main
```
```js
backend: {
name: 'github',
repo: 'owner-name/repo-name', // Path to your GitHub repository
// optional, defaults to main
// branch: 'main'
},
```
</CodeTabs>
## Git Large File Storage (LFS)
Please note that the GitHub backend **does not** support [git-lfs](https://git-lfs.github.com/).

View File

@ -15,7 +15,7 @@ For repositories stored on GitLab, the `gitlab` backend allows CMS users to log
With GitLab's PKCE authorization, users can authenticate with GitLab directly from the client. To do this:
1. Follow the [GitLab docs](https://docs.gitlab.com/ee/integration/oauth_provider.html#adding-an-application-through-the-profile) to add your Static CMS instance as an OAuth application and uncheck the **Confidential** checkbox. For the **Redirect URI**, enter the address where you access Static CMS, for example, `https://www.mysite.com/admin/`. For scope, select `api`.
2. GitLab gives you an **Application ID**. Copy this ID and enter it in your Static CMS `config.yml` file, along with the following settings:
2. GitLab gives you an **Application ID**. Copy this ID and enter it in your Static CMS `config` file, along with the following settings:
| Name | Type | Default | Description |
| --------- | ------ | ------- | ---------------------------------------- |
@ -24,6 +24,7 @@ With GitLab's PKCE authorization, users can authenticate with GitLab directly fr
### Example
<CodeTabs>
```yaml
backend:
name: gitlab
@ -32,6 +33,16 @@ backend:
app_id: your-app-id # Application ID from your GitLab settings
```
```js
backend: {
name: 'gitlab',
repo: 'owner-name/repo-name', // Path to your GitLab repository
auth_type: 'pkce', // Required for pkce
app_id: 'your-app-id', // Application ID from your GitLab settings
},
```
</CodeTabs>
### Self-Hosted GitLab Instance
You can also use PKCE Authorization with a self-hosted GitLab instance. This requires adding `api_root`, `base_url`, and `auth_endpoint` fields:
@ -44,6 +55,7 @@ You can also use PKCE Authorization with a self-hosted GitLab instance. This req
#### Example
<CodeTabs>
```yaml
backend:
name: gitlab
@ -54,3 +66,16 @@ backend:
base_url: https://my-hosted-gitlab-instance.com
auth_endpoint: oauth/authorize
```
```js
backend: {
name: 'gitlab',
repo: 'owner-name/repo-name', // Path to your GitLab repository
auth_type: 'pkce', // Required for pkce
app_id: 'your-app-id', // Application ID from your GitLab settings
api_root: 'https://my-hosted-gitlab-instance.com/api/v4',
base_url: 'https://my-hosted-gitlab-instance.com',
auth_endpoint: 'oauth/authorize',
},
```
</CodeTabs>

View File

@ -152,7 +152,7 @@ Netlify's Identity and Git Gateway services allow you to manage CMS admin users
## Start publishing
It's time to create your first blog post. Login to your site's `/admin/` page and create a new post by clicking New Blog. Add a title, a date and some text. When you click Publish, a new commit will be created in your GitHub repo with this format `Create Blog “year-month-date-title”`.
It's time to create your first blog post. Login to your site's `/admin/` page and create a new post by clicking New Blog. Add a title, a date and some text. When you click Publish, a new commit will be created in your GitHub repo with this format `Create Blog "year-month-date-title"`.
Then Netlify will detect that there was a commit in your repo, and will start rebuilding your project. When your project is deployed you'll be able to see the post you created.

View File

@ -14,6 +14,7 @@ The local backend allows you to use Static CMS with a local git repository, inst
### Example
<CodeTabs>
```yaml
backend:
name: git-gateway
@ -22,6 +23,16 @@ backend:
local_backend: true
```
```js
backend: {
name: 'git-gateway',
},
// when using the default proxy server port
local_backend: true,
```
</CodeTabs>
## Usage
1. Run `npx @staticcms/proxy-server` from the root directory of the above repository.
@ -44,8 +55,9 @@ local_backend: true
PORT=8082
```
2. Update the `local_backend` object in `config.yml` and specify a `url` property to use your custom port number
2. Update the `local_backend` object in `config` and specify a `url` property to use your custom port number
<CodeTabs>
```yaml
backend:
name: git-gateway
@ -56,3 +68,17 @@ local_backend:
# when accessing the local site from a host other than 'localhost' or '127.0.0.1'
allowed_hosts: ['192.168.0.1']
```
```js
backend: {
name: 'git-gateway',
},
local_backend: {
// when using a custom proxy server port
url: 'http://localhost:8082/api/v1',
// when accessing the local site from a host other than 'localhost' or '127.0.0.1'
allowed_hosts: ['192.168.0.1'],
},
```
</CodeTabs>

View File

@ -173,7 +173,7 @@ Netlify's Identity and Git Gateway services allow you to manage CMS admin users
## Start publishing
It's time to create your first blog post. Login to your site's `/admin/` page and create a new post by clicking New Blog. Add a title, a date and some text. When you click Publish, a new commit will be created in your GitHub repo with this format `Create Blog “year-month-date-title”`.
It's time to create your first blog post. Login to your site's `/admin/` page and create a new post by clicking New Blog. Add a title, a date and some text. When you click Publish, a new commit will be created in your GitHub repo with this format `Create Blog "year-month-date-title"`.
Then Netlify will detect that there was a commit in your repo, and will start rebuilding your project. When your project is deployed you'll be able to see the post you created.

View File

@ -1,5 +1,5 @@
---
group: Intro
group: Migration
title: Netlify CMS Migration Guide
weight: 190
---

View File

@ -10,11 +10,20 @@ You can use the `test-repo` backend to try out Static CMS without connecting to
**Note:** The `test-repo` backend can't access your local file system, nor does it connect to a Git repo, thus you won't see any existing files while using it.
To enable this backend, set your backend name to `test-repo` in your Static CMS `config.yml` file.
To enable this backend, set your backend name to `test-repo` in your Static CMS `config` file.
## Example
<CodeTabs>
```yaml
backend:
name: test-repo
```
```js
backend: {
name: 'test-repo',
},
```
</CodeTabs>

View File

@ -0,0 +1,137 @@
---
group: Intro
title: Typescript
weight: 20
---
Static CMS provides first class support for Typescript when using the [Bundling option](/docs/add-to-your-site-bundling).
## Configuration
When using Typescript it is recommended to store your CMS configuration in a typescript file instead of a yaml file to take full advantage of the typings available.
```ts
import type { Config } from '@staticcms/core';
export const config: Config = {
...
}
```
### Custom Widgets
When providing your own widgets, you can extend the `Config` type with your custom widget types to provide proper typing in your config. All custom widget types should extend the `BaseField` interface.
```ts
import type { Config, BaseField } from '@staticcms/core';
export interface HtmlField extends BaseField {
widget: 'html'
default: string;
}
export interface CustomField extends BaseField {
widget: 'custom'
default: number[];
some_other_prop: string;
}
export const config: Config<HtmlField | CustomField> = {
...
}
```
## Widgets
When providing types for custom widgets you need to know the datatype your widget works with and the definition of its `Field`. The examples below assumes the field interface for the widget is in the `config.ts` file in the parent directory.
### Control Component
Control widgets take the `WidgetControlProps` interface as their props.
```tsx
import type { FC } from 'react';
import type { WidgetControlProps } from '@staticcms/core';
import type { HtmlField } from '../config';
const EditorControl: FC<WidgetControlProps<string, HtmlField>> = ({
field, // Typed as a HtmlField
value, // Typed as string | null | undefined
onChange,
openMediaLibrary,
getAsset,
mediaPaths
}) => {
...
};
```
### Preview Component
Control widgets take the `WidgetPreviewProps` interface as their props.
```tsx
import type { FC } from 'react';
import type { WidgetPreviewProps } from '@staticcms/core';
import type { HtmlField } from '../config';
const EditorPreview: FC<WidgetPreviewProps<string, HtmlField>> = ({
field, // Typed as a HtmlField
value, // Typed as string | null | undefined
getAsset,
}) => {
...
};
```
## Preview Templates
When providing types for custom preview templates you need to know the datatype of your collection (or file) that the template is represents. Preview templates take the `TemplatePreviewProps` interface as their props.
```tsx
import { useEffect, useMemo, useState } from 'react';
import type { FC } from 'react';
import type { TemplatePreviewProps } from '@staticcms/core';
interface PostPreviewData {
body: string;
date: string;
title: string;
image?: string;
slug: string;
tags?: string[];
}
const PostPreview: FC<TemplatePreviewProps<PostPreviewData>> = ({ entry, widgetFor, getAsset }) => {
const dateString = useMemo(() => entry.data.date, [entry.data.date]);
const [image, setImage] = useState('');
useEffect(() => {
let alive = true;
const loadImage = async () => {
const loadedImage = await getAsset(entry.data.image ?? '');
if (alive) {
setImage(loadedImage.toString());
}
};
loadImage();
return () => {
alive = false;
};
}, [entry.data.image, getAsset]);
return (
<div>
<h1>{entry.data.title}</h1>
<div>{entry.data.date}</div>
<img title={title} src={image} />
<div>{(entry.data.tags ?? []).join(', ')}</div>
<div>{widgetFor('body')}</div>
</div>
);
};
```

View File

@ -1,7 +1,7 @@
---
group: Intro
group: Migration
title: Updating Your CMS
weight: 10
weight: 100
---
The update procedure for your CMS depends upon the method you used to install Static CMS.

View File

@ -24,8 +24,9 @@ The next and final step is updating your Static CMS configuration file:
2. In the `media_library` object, add the name of the media player under `name`.
3. Add a `config` object under name with a `publicKey` property with your Uploadcare public key as it's value.
Your `config.yml` should now include something like this (except with a real API key):
Your `config` should now include something like this (except with a real API key):
<CodeTabs>
```yaml
media_library:
name: uploadcare
@ -33,13 +34,23 @@ media_library:
publicKey: YOUR_UPLOADCARE_PUBLIC_KEY
```
```js
media_library: {
name: 'uploadcare',
config: {
publicKey: 'YOUR_UPLOADCARE_PUBLIC_KEY',
},
},
```
</CodeTabs>
Once you've finished updating your Static CMS configuration, the Uploadcare widget will appear when using the image or file widgets.
**Note:** You'll need to [register the media libraries yourself](/blog/2019/07/netlify-cms-gatsby-plugin-4-0-0#using-media-libraries-with-netlify-cms-app).
## Configuring the Uploadcare Widget
The Uploadcare widget can be configured with settings that are outlined [in their docs](https://uploadcare.com/docs/file_uploads/widget/options/). The widget itself accepts configuration through global variables and data properties on HTML elements, but with Static CMS you can pass configuration options directly through your `config.yml`.
The Uploadcare widget can be configured with settings that are outlined [in their docs](https://uploadcare.com/docs/file_uploads/widget/options/). The widget itself accepts configuration through global variables and data properties on HTML elements, but with Static CMS you can pass configuration options directly through your `config`.
**Note:** all default values described in Uploadcare's documentation also apply in the Static CMS integration, except for `previewStep`, which is set to `true`. This was done because the preview step provides helpful information like upload status, and provides access to image editing controls. This option can be disabled through the configuration options below.
@ -51,18 +62,33 @@ Global configuration, which is meant to affect the Uploadcare widget at all time
Configuration can also be provided for individual fields that use the media library. The structure is very similar to the global configuration, except the settings are added to an individual `field`. For example:
<CodeTabs>
```yaml
...
fields:
name: cover
label: Cover Image
widget: image
media_library:
config:
multiple: true
previewStep: false
fields:
name: cover
label: Cover Image
widget: image
media_library:
config:
multiple: true
previewStep: false
```
```js
fields: {
name: 'cover',
label: 'Cover Image',
widget: 'image',
media_library: {
config: {
multiple: true,
previewStep: false,
},
},
},
```
</CodeTabs>
## Integration settings
There are several settings that control the behavior of integration with the widget.
@ -70,6 +96,7 @@ There are several settings that control the behavior of integration with the wid
* `autoFilename` (`boolean`) - specify whether to add a filename to the end of the url. Example: `http://ucarecdn.com/:uuid/filename.png`
* `defaultOperations` (`string`) - specify a string added at the end of the url. This could be useful to apply a set of CDN operations to each image, for example resizing or compression. All the possible operations are listed [here](https://uploadcare.com/docs/api_reference/cdn/).
<CodeTabs>
```yaml
media_library:
name: uploadcare
@ -79,3 +106,17 @@ media_library:
autoFiletitle: true
defaultOperations: '/resize/800x600/'
```
```js
media_library: {
name: 'uploadcare',
config: {
publicKey: 'YOUR_UPLOADCARE_PUBLIC_KEY',
},
settings: {
autoFiletitle: true,
defaultOperations: '/resize/800x600/',
},
},
```
</CodeTabs>

View File

@ -20,9 +20,19 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
## Example
<CodeTabs>
```yaml
name: draft
label: Draft
widget: boolean
default: true
```
```js
name: 'draft',
label: 'Draft',
widget: 'boolean',
default: true,
```
</CodeTabs>

View File

@ -24,8 +24,17 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
## Example
<CodeTabs>
```yaml
name: code
label: Code
widget: code
```
```js
name: 'code',
label: 'Code',
widget: 'code',
```
</CodeTabs>

View File

@ -23,14 +23,25 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
## Examples
### Basic
<CodeTabs>
```yaml
name: color
label: Color
widget: color
```
```js
name: 'color',
label: 'Color',
widget: 'color',
```
</CodeTabs>
### Kitchen Sink
<CodeTabs>
```yaml
name: color
label: Color
@ -38,3 +49,13 @@ widget: color
enable_alpha: true
allow_input: true
```
```js
name: 'color',
label: 'Color',
widget: 'color',
enable_alpha: true,
allow_input: true,
```
</CodeTabs>

View File

@ -26,6 +26,7 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
### Date Time Picker
<CodeTabs>
```yaml
name: 'datetime'
label: 'Datetime'
@ -35,8 +36,20 @@ time_format: 'HH:mm' # e.g. 21:07
format: 'yyyy-MM-dd HH:mm' # e.g. 2022-12-24 21:07
```
```js
name: 'datetime',
label: 'Datetime',
widget: 'datetime',
date_format: 'dd.MM.yyyy', // e.g. 24.12.2022
time_format: 'HH:mm', // e.g. 21:07
format: 'yyyy-MM-dd HH:mm', // e.g. 2022-12-24 21:07
```
</CodeTabs>
### Date Picker
<CodeTabs>
```yaml
name: 'date'
label: 'Date'
@ -46,8 +59,20 @@ time_format: false
format: 'yyyy-MM-dd' # e.g. 2022-12-24
```
```js
name: 'date',
label: 'Date',
widget: 'datetime',
date_format: 'dd.MM.yyyy', // e.g. 24.12.2022
time_format: false,
format: 'yyyy-MM-dd', // e.g. 2022-12-24
```
</CodeTabs>
### Time Picker
<CodeTabs>
```yaml
name: 'date'
label: 'Date'
@ -56,3 +81,14 @@ date_format: false
time_format: 'HH:mm' # e.g. 21:07
format: 'HH:mm' # e.g. 21:07
```
```js
name: 'date',
label: 'Date',
widget: 'datetime',
date_format: false,
time_format: 'HH:mm', // e.g. 21:07
format: 'HH:mm', // e.g. 21:07
```
</CodeTabs>

View File

@ -31,6 +31,7 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
## Example
<CodeTabs>
```yaml
name: manual_pdf
label: Manual PDF
@ -41,3 +42,18 @@ media_library:
config:
multiple: true
```
```js
name: 'manual_pdf',
label: 'Manual PDF',
widget: 'file',
default: '/uploads/general-manual.pdf',
media_library: {
choose_url: true,
config: {
multiple: true,
},
},
```
</CodeTabs>

View File

@ -20,9 +20,19 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
## Example
<CodeTabs>
```yaml
name: layout
label: Layout
widget: hidden
default: blog
```
```js
name: 'layout',
label: 'Layout',
widget: 'hidden',
default: 'blog',
```
</CodeTabs>

View File

@ -31,6 +31,7 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
## Example
<CodeTabs>
```yaml
name: thumbnail
label: Featured Image
@ -41,3 +42,18 @@ media_library:
config:
multiple: true
```
```js
name: 'thumbnail',
label: 'Featured Image',
widget: 'image',
default: '/uploads/chocolate-dogecoin.jpg',
media_library: {
choose_url: true,
config: {
multiple: true,
},
},
```
</CodeTabs>

View File

@ -32,6 +32,7 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
### Basic
<CodeTabs>
```yaml
name: testimonials
label: Testimonials
@ -56,8 +57,45 @@ fields:
default: /img/emmet.jpg
```
```js
name: 'testimonials',
label: 'Testimonials',
widget: 'list',
summary: '{{fields.quote}} - {{fields.author.name}}',
fields: [
{
name: 'quote',
label: 'Quote',
widget: 'string',
default: 'Everything is awesome!'
},
{
name: 'author',
label: 'Author',
widget: 'object',
fields: [
{
name: 'name',
label: 'Name',
widget: 'string',
default: 'Emmet'
},
{
name: 'avatar',
label: 'Avatar',
widget: 'image',
default: '/img/emmet.jpg'
},
],
},
],
```
</CodeTabs>
### Allow Additions
<CodeTabs>
```yaml
name: testimonials
label: Testimonials
@ -83,28 +121,97 @@ fields:
default: /img/emmet.jpg
```
```js
name: 'testimonials',
label: 'Testimonials',
widget: 'list',
summary: '{{fields.quote}} - {{fields.author.name}}',
allow_add: false,
fields: [
{
name: 'quote',
label: 'Quote',
widget: 'string',
default: 'Everything is awesome!'
},
{
name: 'author',
label: 'Author',
widget: 'object',
fields: [
{
name: 'name',
label: 'Name',
widget: 'string',
default: 'Emmet'
},
{
name: 'avatar',
label: 'Avatar',
widget: 'image',
default: '/img/emmet.jpg'
},
],
},
],
```
</CodeTabs>
### Default Value
<CodeTabs>
```yaml
- name: galleryImages
label: Gallery
widget: list
fields:
- name: src
label: Source
widget: string
- name: alt
label: Alt Text
widget: string
default:
- src: /img/tennis.jpg
alt: Tennis
- src: /img/footbar.jpg
alt: Football
name: galleryImages
label: Gallery
widget: list
fields:
- name: src
label: Source
widget: string
- name: alt
label: Alt Text
widget: string
default:
- src: /img/tennis.jpg
alt: Tennis
- src: /img/footbar.jpg
alt: Football
```
```js
name: 'galleryImages',
label: 'Gallery',
widget: 'list',
fields: [
{
name: 'src',
label: 'Source',
widget: 'string'
},
{
name: 'alt',
label: 'Alt Text',
widget: 'string'
},
],
default: [
{
src: '/img/tennis.jpg',
alt: 'Tennis'
},
{
src: '/img/footbar.jpg',
alt: 'Football'
},
],
```
</CodeTabs>
### Start Collapsed
<CodeTabs>
```yaml
name: testimonials
label: Testimonials
@ -130,8 +237,46 @@ fields:
default: /img/emmet.jpg
```
```js
name: 'testimonials',
label: 'Testimonials',
widget: 'list',
summary: '{{fields.quote}} - {{fields.author.name}}',
collapsed: false,
fields: [
{
name: 'quote',
label: 'Quote',
widget: 'string',
default: 'Everything is awesome!'
},
{
name: 'author',
label: 'Author',
widget: 'object',
fields: [
{
name: 'name',
label: 'Name',
widget: 'string',
default: 'Emmet'
},
{
name: 'avatar',
label: 'Avatar',
widget: 'image',
default: '/img/emmet.jpg'
},
],
},
],
```
</CodeTabs>
### Min and Max
<CodeTabs>
```yaml
name: testimonials
label: Testimonials
@ -158,8 +303,47 @@ fields:
default: /img/emmet.jpg
```
```js
name: 'testimonials',
label: 'Testimonials',
widget: 'list',
summary: '{{fields.quote}} - {{fields.author.name}}',
min: 1,
max: 3,
fields: [
{
name: 'quote',
label: 'Quote',
widget: 'string',
default: 'Everything is awesome!'
},
{
name: 'author',
label: 'Author',
widget: 'object',
fields: [
{
name: 'name',
label: 'Name',
widget: 'string',
default: 'Emmet'
},
{
name: 'avatar',
label: 'Avatar',
widget: 'image',
default: '/img/emmet.jpg'
},
],
},
],
```
</CodeTabs>
### Add To Top
<CodeTabs>
```yaml
name: testimonials
label: Testimonials
@ -185,8 +369,46 @@ fields:
default: /img/emmet.jpg
```
```js
name: 'testimonials',
label: 'Testimonials',
widget: 'list',
summary: '{{fields.quote}} - {{fields.author.name}}',
add_to_top: true,
fields: [
{
name: 'quote',
label: 'Quote',
widget: 'string',
default: 'Everything is awesome!'
},
{
name: 'author',
label: 'Author',
widget: 'object',
fields: [
{
name: 'name',
label: 'Name',
widget: 'string',
default: 'Emmet'
},
{
name: 'avatar',
label: 'Avatar',
widget: 'image',
default: '/img/emmet.jpg'
},
],
},
],
```
</CodeTabs>
### Typed List
<CodeTabs>
```yaml
name: sections
label: Home Section
@ -229,3 +451,70 @@ types:
widget: text
default: Hello World
```
```js
name: 'sections',
label: 'Home Section',
widget: 'list',
types: [
{
name: 'carousel',
label: 'Carousel',
widget: 'object',
summary: '{{fields.header}}',
fields: [
{
name: 'header',
label: 'Header',
widget: 'string',
default: 'Image Gallery'
},
{
name: 'template',
label: 'Template',
widget: 'string',
default: 'carousel.html'
},
{
name: 'images',
label: 'Images',
widget: 'list',
fields: [
{
name: 'image',
label: 'Image',
widget: 'image'
}
],
},
],
},
{
name: 'spotlight',
label: 'Spotlight',
widget: 'object',
fields: [
{
name: 'header',
label: 'Header',
widget: 'string',
default: 'Spotlight'
},
{
name: 'template',
label: 'Template',
widget: 'string',
default: 'spotlight.html'
},
{
name: 'text',
label: 'Text',
widget: 'text',
default: 'Hello World'
},
],
},
],
```
</CodeTabs>

View File

@ -23,8 +23,17 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
## Example
<CodeTabs>
```yaml
name: location
label: Location
widget: map
```
```js
name: 'location',
label: 'Location',
widget: 'map',
```
</CodeTabs>

View File

@ -33,12 +33,21 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
## Example
<CodeTabs>
```yaml
name: body
label: Blog post content
widget: markdown
```
```js
name: 'body',
label: 'Blog post content',
widget: 'markdown',
```
</CodeTabs>
This would render as:
![Markdown widget example](/img/widgets-markdown.webp)

View File

@ -24,6 +24,7 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
## Example
<CodeTabs>
```yaml
name: 'puppies'
label: 'Puppy Count'
@ -34,3 +35,16 @@ min: 1
max: 101
step: 2
```
```js
name: 'puppies',
label: 'Puppy Count',
widget: 'number',
default: 2,
value_type: 'int',
min: 1,
max: 101,
step: 2,
```
</CodeTabs>

View File

@ -24,6 +24,7 @@ _Please note:_ A default value cannot be set directly on an object widget. Inste
## Example
<CodeTabs>
```yaml
name: 'profile'
label: 'Profile'
@ -57,3 +58,43 @@ fields:
label: Postal Code
widget: string
```
```js
name: 'profile',
label: 'Profile',
widget: 'object',
summary: '{{fields.name}}: {{fields.birthdate}}',
fields: [
{
name: 'public',
label: 'Public',
widget: 'boolean',
default: true
},
{
name: 'name',
label: 'Name',
widget: 'string'
},
{
name: 'birthdate',
label: 'Birthdate',
widget: 'date',
default: '',
format: 'MM/DD/YYYY'
},
{
name: 'address',
label: 'Address',
widget: 'object',
collapsed: true,
fields: [
{ name: 'street', label: 'Street Address', widget: 'string' },
{ name: 'city', label: 'City', widget: 'string' },
{ name: 'post-code', label: 'Postal Code', widget: 'string' },
],
},
],
```
</CodeTabs>

View File

@ -33,6 +33,7 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
_Assuming a separate "authors" collection with "name" and "twitterHandle" fields with subfields "first" and "last" for the "name" field._
<CodeTabs>
```yaml
name: author
label: Post Author
@ -43,12 +44,25 @@ value_field: name.first
display_fields: ['twitterHandle', 'followerCount']
```
```js
name: 'author',
label: 'Post Author',
widget: 'relation',
collection: 'authors',
search_fields: ['name.first', 'twitterHandle'],
value_field: 'name.first',
display_fields: ['twitterHandle', 'followerCount'],
```
</CodeTabs>
The generated UI input will search the authors collection by name and twitterHandle, and display each author's handle and follower count. On selection, the author's name is saved for the field.
### String Template
_Assuming a separate "authors" collection with "name" and "twitterHandle" fields with subfields "first" and "last" for the "name" field._
<CodeTabs>
```yaml
name: author
label: Post Author
@ -59,12 +73,25 @@ value_field: '{{slug}}'
display_fields: ['{{twitterHandle}} - {{followerCount}}']
```
```js
name: 'author',
label: 'Post Author',
widget: 'relation',
collection: 'authors',
search_fields: ['name.first'],
value_field: '{{slug}}',
display_fields: ['{{twitterHandle}} - {{followerCount}}'],
```
</CodeTabs>
The generated UI input will search the authors collection by name, and display each author's handle and follower count. On selection, the author entry slug is saved for the field.
### Referencing A File Collection List Field
_Assuming a separate "relation_files" collection with a file named "cities" with a list field "cities" with subfields "name" and "id"._
<CodeTabs>
```yaml
name: city
label: City
@ -76,4 +103,17 @@ display_fields: ['cities.*.name']
value_field: 'cities.*.id'
```
```js
name: 'city',
label: 'City',
widget: 'relation',
collection: 'relation_files',
file: 'cities',
search_fields: ['cities.*.name'],
display_fields: ['cities.*.name'],
value_field: 'cities.*.id',
```
</CodeTabs>
The generated UI input will search the cities file by city name, and display each city's name. On selection, the city id is saved for the field.

View File

@ -14,18 +14,19 @@ The select widget allows you to pick a string value from a dropdown menu.
For common options, see [Common widget options](/docs/widgets#common-widget-options).
| Name | Type | Default | Description |
| -------- | ------------------------------------------------------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| options | list of strings<br />\| list of numbers<br />\| object with `label` and `value` | | <ul><li>`string` or `number` - The dropdown displays the value directly</li><li>object with `label` and `value` fields - The label displays in the dropdown and the value saves in the file</li></ul> |
| default | string<br />\| number<br />\| list of string<br />\| list of number | `''`<br />`[]` if multiple is `true` | _Optional_. The default value for the field. Accepts a string |
| multiple | boolean | `false` | _Optional_. Allow multiple values/options to be selected |
| min | number | | _Optional_. Minimum number of items. Ignored if **multiple** is `false` |
| max | number | | _Optional_. Maximum number of items; ignored if **multiple** is `false` |
| Name | Type | Default | Description |
| -------- | ----------------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| options | list of strings<br />\| list of numbers<br />\| object of `label` and `value` | | <ul><li>`string` or `number` - The dropdown displays the value directly</li><li>object with `label` and `value` fields - The label displays in the dropdown and the value saves in the file</li></ul> |
| default | string<br />\| number<br />\| list of string<br />\| list of number | `''` or `[]` | _Optional_. The default value for the field. Accepts a string. Defaults to an empty array if `multiple` is `true` |
| multiple | boolean | `false` | _Optional_. Allow multiple values/options to be selected |
| min | number | | _Optional_. Minimum number of items. Ignored if **multiple** is `false` |
| max | number | | _Optional_. Maximum number of items; ignored if **multiple** is `false` |
## Examples
### Options As Strings
<CodeTabs>
```yaml
name: align
label: Align Content
@ -33,6 +34,15 @@ widget: select
options: ['left', 'center', 'right']
```
```js
name: 'align',
label: 'Align Content',
widget: 'select',
options: ['left', 'center', 'right'],
```
</CodeTabs>
Selecting the `center` option, will save the value as:
```yaml
@ -41,6 +51,7 @@ align: center
### Options As Numbers
<CodeTabs>
```yaml
name: align
label: Align Content
@ -48,6 +59,15 @@ widget: select
options: [1, 2, 3]
```
```js
name: 'align',
label: 'Align Content',
widget: 'select',
options: [1, 2, 3],
```
</CodeTabs>
Selecting the `2` option, will save the value as:
```yaml
@ -56,6 +76,7 @@ align: 2
### Options As Objects
<CodeTabs>
```yaml
name: airport-code
label: City
@ -69,6 +90,28 @@ options:
value: HND
```
```js
name: 'airport-code',
label: 'City',
widget: 'select',
options: [
{
label: 'Chicago',
value: 'ORD'
},
{
label: 'Paris',
value: 'CDG'
},
{
label: 'Tokyo',
value: 'HND'
},
],
```
</CodeTabs>
Selecting the `Chicago` option, will save the value as:
```yaml
@ -77,6 +120,7 @@ airport-code: ORD
### Multiple
<CodeTabs>
```yaml
name: 'tags'
label: 'Tags'
@ -86,8 +130,20 @@ options: ['Design', 'UX', 'Dev']
default: ['Design']
```
```js
name: 'tags',
label: 'Tags',
widget: 'select',
multiple: true,
options: ['Design', 'UX', 'Dev'],
default: ['Design'],
```
</CodeTabs>
### Min and Max
<CodeTabs>
```yaml
name: 'tags'
label: 'Tags'
@ -98,3 +154,16 @@ max: 3
options: ['Design', 'UX', 'Dev']
default: ['Design']
```
```js
name: 'tags',
label: 'Tags',
widget: 'select',
multiple: true,
min: 1,
max: 3,
options: ['Design', 'UX', 'Dev'],
default: ['Design'],
```
</CodeTabs>

View File

@ -20,8 +20,17 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
## Example
<CodeTabs>
```yaml
name: title
label: Title
widget: string
```
```js
name: 'title',
label: 'Title',
widget: 'string',
```
</CodeTabs>

View File

@ -20,8 +20,17 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
## Example
<CodeTabs>
```yaml
name: description
label: Description
widget: text
```
```js
name: 'description',
label: 'Description',
widget: 'text',
```
</CodeTabs>

View File

@ -6,7 +6,7 @@ weight: 0
Widgets define the data type and interface for entry fields. Static CMS comes with several built-in widgets. Click the widget names in the sidebar to jump to specific widget details. You can also [create your own](/docs/custom-widgets)!
Widgets are specified as collection fields in the Static CMS `config.yml` file. Note that [YAML syntax](https://en.wikipedia.org/wiki/YAML#Basic_components) allows lists and objects to be written in block or inline style, and the code samples below include a mix of both.
Widgets are specified as collection fields in the Static CMS `config` file. Note that [YAML syntax](https://en.wikipedia.org/wiki/YAML#Basic_components) allows lists and objects to be written in block or inline style, and the code samples below include a mix of both.
To see working examples of all of the built-in widgets, try making a 'Kitchen Sink' collection item on the [CMS demo site](https://static-static-cms-demo.netlify.app). (No login required: click the login button and the CMS will open.) You can refer to the demo [configuration code](https://github.com/StaticJsCMS/static-cms/blob/main/dev-test/config.yml) to see how each field was configured.
@ -48,9 +48,19 @@ The following options are available on all fields:
### Example
<CodeTabs>
```yaml
name: title
label: Title
widget: string
pattern: ['.{12,}', 'Must have at least 12 characters']
```
```js
name: 'title',
label: 'Title',
widget: 'string',
pattern: ['.{12,}', 'Must have at least 12 characters'],
```
</CodeTabs>

View File

@ -154,7 +154,7 @@ Exception: Use passive voice if active voice leads to an awkward construction.
### Use simple and direct language
Use simple and direct language. Avoid using unnecessary phrases, such as saying “please.”
Use simple and direct language. Avoid using unnecessary phrases, such as saying "please."
> Do: To create an entry, …
@ -170,7 +170,7 @@ _____
> Don't: With this next command, we'll view the fields.
### Address the reader as “you”
### Address the reader as "you"
> Do: You can create a Deployment by …
@ -195,13 +195,13 @@ _____
> Don't: i.e., …
_____
Exception: Use “etc.” for et cetera.
Exception: Use "etc." for et cetera.
## Patterns to avoid
### Avoid using “we”
### Avoid using "we"
Using “we” in a sentence can be confusing, because the reader might not know whether they're part of the “we” you're describing.
Using "we" in a sentence can be confusing, because the reader might not know whether they're part of the "we" you're describing.
> Do: Version 1.4 includes …
@ -236,7 +236,7 @@ Avoid making promises or giving hints about the future. If you need to talk abou
### Avoid statements that will soon be out of date
Avoid words like “currently” and “new.” A feature that is new today will not be new in a few months.
Avoid words like "currently" and "new." A feature that is new today will not be new in a few months.
> Do: In version 1.4, …

View File

@ -3,7 +3,11 @@
"docs": [
{
"name": "Intro",
"title": "Intro to Static CMS"
"title": "Getting Started"
},
{
"name": "Migration",
"title": "Migration Guides"
},
{
"name": "Backends",

View File

@ -24,7 +24,8 @@
"react-dom": "18.2.0",
"react-schemaorg": "2.0.0",
"remark-gfm": "3.0.1",
"schema-dts": "1.1.0"
"schema-dts": "1.1.0",
"yaml": "2.1.3"
},
"devDependencies": {
"@babel/core": "7.19.6",
@ -33,6 +34,7 @@
"@next/eslint-plugin-next": "12.3.1",
"@types/js-yaml": "4.0.5",
"@types/node": "18.11.3",
"@types/prettier": "2.7.1",
"@types/prismjs": "1.26.0",
"@types/react": "18.0.21",
"@types/react-dom": "18.0.6",
@ -44,7 +46,6 @@
"eslint-config-prettier": "8.5.0",
"eslint-plugin-babel": "5.3.1",
"eslint-plugin-unicorn": "44.0.2",
"prettier": "2.7.1",
"typescript": "4.8.4",
"webpack": "5.74.0"
}

View File

@ -22,7 +22,6 @@ const DocsContent = styled('div')(
color: #9b9b9b;
}
& div,
& p {
line-height: 1.5rem;
margin: 0 0 16px;
@ -30,10 +29,13 @@ const DocsContent = styled('div')(
word-break: break-word;
}
& div,
& p:not(:first-of-type) {
margin-top: 8px;
}
& pre + p:not(:first-of-type) {
margin-top: 20px;
}
& :not(h1,h2,h3,h4,h5,h6) a {
color: ${theme.palette.secondary.main};
@ -163,12 +165,15 @@ const DocsContent = styled('div')(
margin-top: 0;
}
& pre {
.dark & pre,
.dark & pre[class*='language-'],
.light & pre,
.light & pre[class*='language-'] {
display: block;
line-height: 1.25rem;
padding: 1rem;
overflow: auto;
margin: 1.75rem 0 0 0;
margin: 0;
}
& pre code {

View File

@ -0,0 +1,138 @@
import Box from '@mui/material/Box';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import Prism from 'prismjs';
import { isValidElement, useMemo, useState } from 'react';
import { isNotEmpty } from '../../../util/string.util';
import type { Grammar } from 'prismjs';
import type { ReactElement, ReactNode, SyntheticEvent } from 'react';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && <Box>{children}</Box>}
</div>
);
}
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
interface CodeLanguage {
title: string;
grammar: Grammar;
language: string;
}
const supportedLanguages: Record<string, CodeLanguage> = {
'language-yaml': {
title: 'Yaml',
grammar: Prism.languages.yaml,
language: 'yaml',
},
'language-js': {
title: 'JavaScript',
grammar: Prism.languages.javascript,
language: 'javascript',
},
};
interface TabData {
title: string;
className: string;
content: string;
}
interface CodeTabsProps {
children?: ReactNode;
}
const CodeTabs = ({ children }: CodeTabsProps) => {
const [value, setValue] = useState(0);
const handleChange = (_event: SyntheticEvent, newValue: number) => {
setValue(newValue);
};
const tabs = useMemo(() => {
if (!children || !Array.isArray(children)) {
return [];
}
return children
.filter((child: ReactNode) => isValidElement(child) && child.type === 'pre')
.map((child: ReactElement) => child.props.children)
.filter((subChild: ReactNode) => isValidElement(subChild) && subChild.type === 'code')
.map((code: ReactElement) => {
if (!(code.props.className in supportedLanguages)) {
return false;
}
const language = supportedLanguages[code.props.className];
console.log(code.props.children);
return {
title: language.title,
className: code.props.className,
content: typeof code.props.children === 'string' && isNotEmpty(code.props.children)
? Prism.highlight(code.props.children, language.grammar, language.language)
: '',
};
})
.filter(Boolean) as TabData[];
}, [children]);
if (tabs.length === 0) {
return null;
}
return (
<Box sx={{ width: '100%', margin: '8px 0 16px' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs
value={value}
onChange={handleChange}
aria-label="basic tabs example"
sx={{ '.MuiTabs-root': { margin: 0 }, '.MuiTabs-flexContainer': { margin: 0 } }}
>
{tabs.map((tabData, index) => (
<Tab key={tabData.className} label={tabData.title} {...a11yProps(index)} />
))}
</Tabs>
</Box>
{tabs.map((tabData, index) => (
<TabPanel key={tabData.className} value={value} index={index}>
<pre className={tabData.className}>
<code
className={tabData.className}
dangerouslySetInnerHTML={{ __html: tabData.content }}
/>
</pre>
</TabPanel>
))}
</Box>
);
};
export default CodeTabs;

View File

@ -2,7 +2,7 @@ const useAnchor = (text: string) => {
return text
.trim()
.toLowerCase()
.replace(/[^a-z0-9 ]/g, '')
.replace(/[^a-z0-9 \-_]/g, '')
.replace(/[ ]/g, '-');
};

View File

@ -6,6 +6,8 @@ import type { ReactNode } from 'react';
const StyledTableContainer = styled(TableContainer)(
({ theme }) => `
margin-bottom: 16px;
& td {
color: ${theme.palette.text.secondary};
}

View File

@ -13,10 +13,10 @@ const TableBodyCell = ({ children }: TableBodyCellProps) => {
scope="row"
sx={{
padding: '16px 12px',
'&:first-child, &:first-child': {
'&:first-of-type, &:first-of-type': {
paddingLeft: 0,
},
'&:last-child, &:last-child': {
'&:last-of-type, &:last-of-type': {
paddingRight: 0,
},
}}

View File

@ -12,10 +12,10 @@ const TableHeaderCell = ({ children }: TableHeaderCellProps) => {
sx={{
fontWeight: 600,
padding: '16px 12px',
'&:first-child, &:first-child': {
'&:first-of-type, &:first-of-type': {
paddingLeft: 0,
},
'&:last-child, &:last-child': {
'&:last-of-type, &:last-of-type': {
paddingRight: 0,
},
}}

View File

@ -15,14 +15,13 @@ import type { ReactNode } from 'react';
import type { DocsGroup } from '../../interface';
const StyledPageContentWrapper = styled('div')`
display: flex;
flex-direction: column;
align-items: center;
display: block;
height: calc(100vh - 72px);
width: 100%;
position: relative;
top: 72px;
overflow-y: auto;
overflow-x: hidden;
`;
export interface PageProps {

View File

@ -23,6 +23,7 @@ const MobileNavLink = ({ link, onClick }: MobileNavLinkProps) => {
return (
<Link href={url} target={url.startsWith('http') ? '_blank' : undefined}>
<ListItemButton
href={url}
sx={{ paddingLeft: '24px', paddingTop: '4px', paddingBottom: '4px' }}
onClick={onClick}
selected={selected}

View File

@ -15,11 +15,15 @@ import type { PaletteMode } from '@mui/material';
import type { AppProps } from 'next/app';
require('prismjs/components/prism-javascript');
require('prismjs/components/prism-typescript');
require('prismjs/components/prism-css');
require('prismjs/components/prism-jsx');
require('prismjs/components/prism-tsx');
require('prismjs/components/prism-yaml');
require('prismjs/components/prism-json');
require('prismjs/components/prism-toml');
require('prismjs/components/prism-markup-templating');
require('prismjs/components/prism-handlebars');
function MyApp({ Component, pageProps }: AppProps) {
const [mode, setMode] = useState<PaletteMode>('dark');

View File

@ -6,6 +6,7 @@ import remarkGfm from 'remark-gfm';
import Anchor from '../../components/docs/components/Anchor';
import Blockquote from '../../components/docs/components/Blockquote';
import CodeTabs from '../../components/docs/components/CodeTabs';
import Header2 from '../../components/docs/components/headers/Header2';
import Header3 from '../../components/docs/components/headers/Header3';
import Header4 from '../../components/docs/components/headers/Header4';
@ -103,6 +104,7 @@ const Docs = ({ docsGroups, title, slug, description = '', source }: DocsProps)
h5: Header5,
h6: Header6,
blockquote: Blockquote,
CodeTabs,
table: DocsTable,
thead: TableHead,
tbody: TableBody,

View File

@ -6,6 +6,10 @@ img {
max-width: 100%;
}
#__next {
position: relative;
}
/* PrismJS 1.29.0 (Dark)
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+json+toml+typescript+yaml */
.dark code[class*='language-'],

6
website/test.ts Normal file
View File

@ -0,0 +1,6 @@
export const test = {
media_library: {
name: 'cloudinary',
config: { cloud_name: 'your_cloud_name', api_key: 'your_api_key' },
},
};

View File

@ -1541,6 +1541,11 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/prettier@2.7.1":
version "2.7.1"
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e"
integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==
"@types/prismjs@1.26.0":
version "1.26.0"
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.0.tgz#a1c3809b0ad61c62cac6d4e0c56d610c910b7654"
@ -4643,11 +4648,6 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier@2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
@ -5887,6 +5887,11 @@ yallist@^4.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.1.3.tgz#9b3a4c8aff9821b696275c79a8bee8399d945207"
integrity sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==
yaml@^1.10.0:
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"