Add Strapi CMS with Article and Category content types
Article is deliberately minimal: title, slug, Markdown body, cover image and category. No Author content type — the administrator is the only writer. The bootstrap hook grants the Public role find/findOne on the two content types and disables self-registration, so the public API is read-only and the site has no front-end accounts.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import type { StrapiApp } from '@strapi/strapi/admin';
|
||||
|
||||
export default {
|
||||
config: {
|
||||
locales: [
|
||||
// 'ar',
|
||||
// 'fr',
|
||||
// 'cs',
|
||||
// 'de',
|
||||
// 'da',
|
||||
// 'es',
|
||||
// 'he',
|
||||
// 'id',
|
||||
// 'it',
|
||||
// 'ja',
|
||||
// 'ko',
|
||||
// 'ms',
|
||||
// 'nl',
|
||||
// 'no',
|
||||
// 'pl',
|
||||
// 'pt-BR',
|
||||
// 'pt',
|
||||
// 'ru',
|
||||
// 'sk',
|
||||
// 'sv',
|
||||
// 'th',
|
||||
// 'tr',
|
||||
// 'uk',
|
||||
// 'vi',
|
||||
// 'zh-Hans',
|
||||
// 'zh',
|
||||
],
|
||||
},
|
||||
bootstrap(app: StrapiApp) {
|
||||
console.log(app);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["../plugins/**/admin/src/**/*", "./"],
|
||||
"exclude": ["node_modules/", "build/", "dist/", "**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { mergeConfig, type UserConfig } from 'vite';
|
||||
|
||||
export default (config: UserConfig) => {
|
||||
// Important: always return the modified config
|
||||
return mergeConfig(config, {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': '/src',
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"kind": "collectionType",
|
||||
"collectionName": "articles",
|
||||
"info": {
|
||||
"singularName": "article",
|
||||
"pluralName": "articles",
|
||||
"displayName": "Article",
|
||||
"description": "Blog article"
|
||||
},
|
||||
"options": {
|
||||
"draftAndPublish": true
|
||||
},
|
||||
"attributes": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"maxLength": 160
|
||||
},
|
||||
"slug": {
|
||||
"type": "uid",
|
||||
"targetField": "title",
|
||||
"required": true
|
||||
},
|
||||
"content": {
|
||||
"type": "richtext",
|
||||
"required": true
|
||||
},
|
||||
"cover": {
|
||||
"type": "media",
|
||||
"multiple": false,
|
||||
"allowedTypes": ["images"]
|
||||
},
|
||||
"category": {
|
||||
"type": "relation",
|
||||
"relation": "manyToOne",
|
||||
"target": "api::category.category",
|
||||
"inversedBy": "articles"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { factories } from '@strapi/strapi';
|
||||
|
||||
export default factories.createCoreController('api::article.article');
|
||||
@@ -0,0 +1,3 @@
|
||||
import { factories } from '@strapi/strapi';
|
||||
|
||||
export default factories.createCoreRouter('api::article.article');
|
||||
@@ -0,0 +1,3 @@
|
||||
import { factories } from '@strapi/strapi';
|
||||
|
||||
export default factories.createCoreService('api::article.article');
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"kind": "collectionType",
|
||||
"collectionName": "categories",
|
||||
"info": {
|
||||
"singularName": "category",
|
||||
"pluralName": "categories",
|
||||
"displayName": "Category",
|
||||
"description": "Article category"
|
||||
},
|
||||
"options": {
|
||||
"draftAndPublish": false
|
||||
},
|
||||
"attributes": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"unique": true
|
||||
},
|
||||
"slug": {
|
||||
"type": "uid",
|
||||
"targetField": "name",
|
||||
"required": true
|
||||
},
|
||||
"articles": {
|
||||
"type": "relation",
|
||||
"relation": "oneToMany",
|
||||
"target": "api::article.article",
|
||||
"mappedBy": "category"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { factories } from '@strapi/strapi';
|
||||
|
||||
export default factories.createCoreController('api::category.category');
|
||||
@@ -0,0 +1,3 @@
|
||||
import { factories } from '@strapi/strapi';
|
||||
|
||||
export default factories.createCoreRouter('api::category.category');
|
||||
@@ -0,0 +1,3 @@
|
||||
import { factories } from '@strapi/strapi';
|
||||
|
||||
export default factories.createCoreService('api::category.category');
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Core } from '@strapi/strapi';
|
||||
|
||||
/** Read-only endpoints the public website needs. Nothing else is granted. */
|
||||
const PUBLIC_READ_ACTIONS = ['article', 'category'].flatMap((type) => [
|
||||
`api::${type}.${type}.find`,
|
||||
`api::${type}.${type}.findOne`,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Grants the Public role read access to the blog content types, so a fresh
|
||||
* deployment serves content without anyone clicking through the admin panel.
|
||||
* Existing permissions are left untouched.
|
||||
*/
|
||||
async function grantPublicReadAccess(strapi: Core.Strapi) {
|
||||
const publicRole = await strapi
|
||||
.query('plugin::users-permissions.role')
|
||||
.findOne({ where: { type: 'public' } });
|
||||
|
||||
if (!publicRole) return;
|
||||
|
||||
for (const action of PUBLIC_READ_ACTIONS) {
|
||||
const existing = await strapi
|
||||
.query('plugin::users-permissions.permission')
|
||||
.findOne({ where: { action, role: publicRole.id } });
|
||||
|
||||
if (!existing) {
|
||||
await strapi
|
||||
.query('plugin::users-permissions.permission')
|
||||
.create({ data: { action, role: publicRole.id } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The site has no front-end accounts: only the administrators authoring content
|
||||
* in the admin panel. Self-registration is therefore closed, so nobody can
|
||||
* create an Authenticated user through the public API.
|
||||
*/
|
||||
async function disablePublicSignUp(strapi: Core.Strapi) {
|
||||
const store = strapi.store({ type: 'plugin', name: 'users-permissions', key: 'advanced' });
|
||||
const advanced = ((await store.get({ key: 'advanced' })) ?? {}) as Record<string, unknown>;
|
||||
|
||||
if (advanced.allow_register !== false) {
|
||||
await store.set({ key: 'advanced', value: { ...advanced, allow_register: false } });
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
register() {},
|
||||
|
||||
async bootstrap({ strapi }: { strapi: Core.Strapi }) {
|
||||
await grantPublicReadAccess(strapi);
|
||||
await disablePublicSignUp(strapi);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user