├── .prettierignore
├── .DS_Store
├── .eslintrc
├── .gitignore
├── modules
└── @apostrophecms
│ └── blog-page
│ ├── views
│ ├── show.html
│ ├── index.html
│ └── filters.html
│ └── index.js
├── i18n
├── en.json
├── fr.json
├── it.json
├── pt-BR.json
├── de.json
├── sk.json
└── es.json
├── CHANGELOG.md
├── LICENSE.md
├── package.json
├── index.js
├── queries.js
└── README.md
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/*.html
2 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/blog/main/.DS_Store
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["apostrophe"],
3 | "globals": {
4 | "apos": true
5 | },
6 | "rules": {
7 | "no-var": "error",
8 | "no-console": 0
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore MacOS X metadata forks (fusefs)
2 | ._*
3 | package-lock.json
4 | .DS_Store
5 | node_modules
6 |
7 | # Never commit a CSS map file, anywhere
8 | *.css.map
9 |
10 | # vim swp files
11 | .*.sw*
--------------------------------------------------------------------------------
/modules/@apostrophecms/blog-page/views/show.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 |
3 | {% block main %}
4 |
{{ data.piece.title }}
5 |
6 | {{ data.piece.publishedAt | date('MMMM D, YYYY') }}
7 |
8 | {% endblock %}
--------------------------------------------------------------------------------
/i18n/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Blog post",
3 | "pluralLabel": "Blog post",
4 | "publishedAt": "Publication Date",
5 | "futureArticles": "Future Posts",
6 | "future": "Future",
7 | "past": "Past",
8 | "both": "Both",
9 | "page": "Blog Page",
10 | "filters": "Filters",
11 | "filterAll": "All",
12 | "filterYear": "Year",
13 | "filterMonth": "Month",
14 | "filterDay": "Day",
15 | "releasedOn": "Released on"
16 | }
17 |
--------------------------------------------------------------------------------
/i18n/fr.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Article de blog",
3 | "pluralLabel": "Articles de blog",
4 | "publishedAt": "Date de publication",
5 | "futureArticles": "Articles futurs",
6 | "future": "Futur",
7 | "past": "Passé",
8 | "both": "Les deux",
9 | "page": "Page de blog",
10 | "filters": "Filtres",
11 | "filterAll": "Tous",
12 | "filterYear": "Année",
13 | "filterMonth": "Mois",
14 | "filterDay": "Jour",
15 | "releasedOn": "Publiée le"
16 | }
17 |
--------------------------------------------------------------------------------
/i18n/it.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Post del blog",
3 | "pluralLabel": "Post del blog",
4 | "publishedAt": "Data di pubblicazione",
5 | "futureArticles": "Post futuri",
6 | "future": "Futuro",
7 | "past": "Passato",
8 | "both": "Entrambi",
9 | "page": "Pagina del blog",
10 | "filters": "Filtri",
11 | "filterAll": "Tutti",
12 | "filterYear": "Anno",
13 | "filterMonth": "Mese",
14 | "filterDay": "Giorno",
15 | "releasedOn": "Pubblicato il"
16 | }
17 |
--------------------------------------------------------------------------------
/i18n/pt-BR.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Postagem do Blog",
3 | "pluralLabel": "Postagens do Blog",
4 | "publishedAt": "Data de Publicação",
5 | "futureArticles": "Posts Futuros",
6 | "future": "Futuro",
7 | "past": "Passado",
8 | "both": "Ambos",
9 | "page": "Página do Blog",
10 | "filters": "Filtros",
11 | "filterAll": "Todos",
12 | "filterYear": "Ano",
13 | "filterMonth": "Mês",
14 | "filterDay": "Dia",
15 | "releasedOn": "Lançado em"
16 | }
17 |
--------------------------------------------------------------------------------
/i18n/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Blogbeitrag",
3 | "pluralLabel": "Blogbeiträge",
4 | "publishedAt": "Veröffentlichungsdatum",
5 | "futureArticles": "Zukünftige Beiträge",
6 | "future": "Zukunft",
7 | "past": "Vergangenheit",
8 | "both": "Beide",
9 | "page": "Blogseite",
10 | "filters": "Filter",
11 | "filterAll": "Alle",
12 | "filterYear": "Jahr",
13 | "filterMonth": "Monat",
14 | "filterDay": "Tag",
15 | "releasedOn": "Veröffentlicht am"
16 | }
17 |
--------------------------------------------------------------------------------
/i18n/sk.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Príspevok na blogu",
3 | "pluralLabel": "Príspevky na blogu",
4 | "publishedAt": "Dátum publikovania",
5 | "futureArticles": "Budúce príspevky",
6 | "future": "Budúce",
7 | "past": "Minulé",
8 | "both": "Obe",
9 | "page": "Stránka blogu",
10 | "filters": "Filtre",
11 | "filterAll": "Všetko",
12 | "filterYear": "Rok",
13 | "filterMonth": "Mesiac",
14 | "filterDay": "Deň",
15 | "releasedOn": "Publikované dňa"
16 | }
17 |
--------------------------------------------------------------------------------
/i18n/es.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Publicación del blog",
3 | "pluralLabel": "Publicaciones del blog",
4 | "publishedAt": "Fecha de publicación",
5 | "futureArticles": "Publicaciones futuras",
6 | "future": "Futuro",
7 | "past": "Pasado",
8 | "both": "Ambos",
9 | "page": "Página del blog",
10 | "filters": "Filtros",
11 | "filterAll": "Todos",
12 | "filterYear": "Año",
13 | "filterMonth": "Mes",
14 | "filterDay": "Día",
15 | "releasedOn": "Publicado en"
16 | }
17 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/blog-page/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/piece-page-type',
3 |
4 | options: {
5 | label: 'aposBlog:page',
6 | piecesFilters: [ { name: 'year' }, { name: 'month' }, { name: 'day' } ]
7 | },
8 |
9 | extendMethods(self) {
10 | return {
11 | indexQuery(_super, req) {
12 | return _super(req).future(false);
13 | },
14 | showQuery(_super, req) {
15 | return _super(req).future(false);
16 | }
17 | };
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/blog-page/views/index.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 |
3 | {% import "filters.html" as filters %}
4 | {% import "@apostrophecms/pager:macros.html" as pager with context %}
5 |
6 | {% block main %}
7 | {{ __t('aposBlog:filters') }}
8 |
9 | {% render filters.render({
10 | filters: data.piecesFilters,
11 | query: data.query,
12 | url: data.page._url
13 | }) %}
14 |
15 | {{ __t('aposBlog:pluralLabel') }}
16 |
17 | {% for piece in data.pieces %}
18 |
19 |
20 | {{ __t('aposBlog:releasedOn') }} {{ piece.publishedAt | date('MMMM D, YYYY') }}
21 |
22 |
25 |
26 | {% endfor %}
27 |
28 | {{ pager.render({ page: data.currentPage, total: data.totalPages }, data.url) }}
29 | {% endblock %}
30 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.0.6 (2025-09-03)
4 |
5 | * README improvemeents
6 |
7 | ## 1.0.5 (2025-04-16)
8 |
9 | ### Changes
10 |
11 | - Updates the documentation. No code changes.
12 |
13 | ### Fixed
14 |
15 | * Fix the result sorting and pagination issues due to unreliable sorting values.
16 |
17 | ## 1.0.4 2023-09-27
18 |
19 | ### Changes
20 |
21 | - Removes the outdated `safeFor` property from the custom queries.
22 |
23 | ## 1.0.3 - 2023-02-17
24 |
25 | ### Fixed
26 |
27 | - Fixes `future` filter when "both" choice is selected.
28 |
29 | ## 1.0.2 - 2023-02-01
30 |
31 | ### Fixed
32 |
33 | - The npm home page and readme links were corrected. Thanks to community member [zenflow](https://github.com/zenflow). No code changes.
34 |
35 | ## 1.0.1 - 2022-11-28
36 |
37 | ### Fixed
38 |
39 | - Documented the need to activate the bundle in `app.js` separately from activating the modules.
40 |
41 | ## 1.0.0 - 2022-10-03
42 |
43 | ### Added
44 |
45 | - Initial release for Apostrophe 3 with support for blog pieces and blog piece pages.
46 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2022 Apostrophe Technologies
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@apostrophecms/blog",
3 | "version": "1.0.6",
4 | "description": "Blog module for ApostropheCMS websites",
5 | "main": "index.js",
6 | "scripts": {
7 | "lint": "npm run eslint",
8 | "eslint": "eslint .",
9 | "test": "npm run lint"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/apostrophecms/blog.git"
14 | },
15 | "homepage": "https://github.com/apostrophecms/blog#readme",
16 | "author": "Apostrophe Technologies",
17 | "license": "MIT",
18 | "dependencies": {
19 | "dayjs": "^1.10.7"
20 | },
21 | "devDependencies": {
22 | "eslint": "^7.9.0",
23 | "eslint-config-apostrophe": "^3.4.0",
24 | "eslint-config-standard": "^14.1.1",
25 | "eslint-plugin-import": "^2.22.0",
26 | "eslint-plugin-node": "^11.1.0",
27 | "eslint-plugin-promise": "^4.2.1",
28 | "eslint-plugin-standard": "^4.0.1"
29 | },
30 | "prettier": {
31 | "trailingComma": "none",
32 | "tabWidth": 2,
33 | "semi": true,
34 | "singleQuote": true,
35 | "bracketSpacing": true
36 | },
37 | "publishConfig": {
38 | "access": "public"
39 | }
40 | }
--------------------------------------------------------------------------------
/modules/@apostrophecms/blog-page/views/filters.html:
--------------------------------------------------------------------------------
1 | {%- macro here(url, changes) -%}
2 | {{ url | build({
3 | year: data.query.year,
4 | month: data.query.month,
5 | day: data.query.day
6 | }, changes) }}
7 | {%- endmacro -%}
8 |
9 | {% fragment render(data) %}
10 | {{ __t('aposBlog:filterYear') }}
11 |
21 |
22 | {{ __t('aposBlog:filterMonth') }}
23 |
33 |
34 | {{ __t('aposBlog:filterDay') }}
35 |
45 | {% endfragment %}
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const dayjs = require('dayjs');
2 | const queries = require('./queries');
3 |
4 | module.exports = {
5 | extend: '@apostrophecms/piece-type',
6 | bundle: {
7 | directory: 'modules',
8 | modules: [ '@apostrophecms/blog-page' ]
9 | },
10 | options: {
11 | label: 'aposBlog:label',
12 | pluralLabel: 'aposBlog:pluralLabel',
13 | sort: {
14 | publishedAt: -1,
15 | createdAt: -1
16 | },
17 | i18n: {
18 | ns: 'aposBlog',
19 | browser: true
20 | }
21 | },
22 | columns: {
23 | add: {
24 | publishedAt: {
25 | label: 'aposBlog:publishedAt'
26 | }
27 | }
28 | },
29 | fields: {
30 | add: {
31 | publishedAt: {
32 | label: 'aposBlog:publishedAt',
33 | type: 'date',
34 | required: true
35 | }
36 | },
37 | group: {
38 | basics: {
39 | fields: [ 'publishedAt' ]
40 | }
41 | }
42 | },
43 | filters: {
44 | add: {
45 | future: {
46 | label: 'aposBlog:futureArticles',
47 | def: null
48 | },
49 | year: {
50 | label: 'aposBlog:filterYear',
51 | def: null
52 | },
53 | month: {
54 | label: 'aposBlog:filterMonth',
55 | def: null
56 | },
57 | day: {
58 | label: 'aposBlog:filterDay',
59 | def: null
60 | }
61 | }
62 | },
63 | queries,
64 | extendMethods(self) {
65 | return {
66 | newInstance(_super) {
67 | const instance = _super();
68 | if (!instance.publishedAt) {
69 | instance.publishedAt = dayjs().format('YYYY-MM-DD');
70 | }
71 |
72 | return instance;
73 | }
74 | };
75 | }
76 | };
77 |
--------------------------------------------------------------------------------
/queries.js:
--------------------------------------------------------------------------------
1 | const dayjs = require('dayjs');
2 |
3 | module.exports = (self, query) => {
4 | return {
5 | builders: {
6 | future: {
7 | def: null,
8 | finalize() {
9 | let future = query.get('future');
10 |
11 | if (!self.apos.permission.can(query.req, 'edit', self.name, 'draft')) {
12 | future = false;
13 | }
14 |
15 | if (future === null) {
16 | return;
17 | }
18 |
19 | const today = dayjs().format('YYYY-MM-DD');
20 | if (future) {
21 | query.and({ publishedAt: { $gte: today } });
22 | } else {
23 | query.and({ publishedAt: { $lte: today } });
24 | }
25 | },
26 | launder(value) {
27 | return self.apos.launder.booleanOrNull(
28 | value === '' ? null : value
29 | );
30 | },
31 | choices() {
32 | return [
33 | {
34 | value: null,
35 | label: 'aposBlog:both'
36 | },
37 | {
38 | value: true,
39 | label: 'aposBlog:future'
40 | },
41 | {
42 | value: false,
43 | label: 'aposBlog:past'
44 | }
45 | ];
46 | }
47 | },
48 |
49 | // Filter by year, in YYYY format.
50 | year: {
51 | def: null,
52 | finalize() {
53 | const year = query.get('year');
54 | if (!year) {
55 | return;
56 | }
57 |
58 | query.and({ publishedAt: { $regex: '^' + year } });
59 | },
60 | launder(value) {
61 | const year = self.apos.launder.string(value);
62 |
63 | if (!year.match(/^\d\d\d\d$/)) {
64 | return '';
65 | }
66 |
67 | return year;
68 | },
69 | async choices() {
70 | const allDates = await query.toDistinct('publishedAt');
71 | const years = [
72 | {
73 | value: null,
74 | label: 'aposBlog:filterAll'
75 | }
76 | ];
77 |
78 | for (const eachDate of allDates) {
79 | const year = eachDate.substr(0, 4);
80 | if (!years.find((e) => e.value === year)) {
81 | years.push({
82 | value: year,
83 | label: year
84 | });
85 | }
86 | }
87 | years.sort().reverse();
88 |
89 | return years;
90 | }
91 | },
92 |
93 | // Filter by month, in YYYY-MM format.
94 | month: {
95 | def: null,
96 | finalize() {
97 | const month = query.get('month');
98 |
99 | if (!month) {
100 | return;
101 | }
102 |
103 | query.and({ publishedAt: { $regex: '^' + month } });
104 | },
105 | launder(value) {
106 | const month = self.apos.launder.string(value);
107 |
108 | if (!month.match(/^\d\d\d\d-\d\d$/)) {
109 | return null;
110 | }
111 |
112 | return month;
113 | },
114 | async choices() {
115 | const allDates = await query.toDistinct('publishedAt');
116 | const months = [
117 | {
118 | value: null,
119 | label: 'aposBlog:filterAll'
120 | }
121 | ];
122 |
123 | for (const eachDate of allDates) {
124 | const month = eachDate.substr(0, 7);
125 | if (!months.find((e) => e.value === month)) {
126 | months.push({
127 | value: month,
128 | label: month
129 | });
130 | }
131 | }
132 | months.sort().reverse();
133 |
134 | return months;
135 | }
136 | },
137 |
138 | // Filter by day, in YYYY-MM-DD format.
139 | day: {
140 | def: null,
141 | finalize() {
142 | const day = query.get('day');
143 |
144 | if (!day) {
145 | return;
146 | }
147 |
148 | query.and({ publishedAt: day });
149 | },
150 | launder(value) {
151 | const day = self.apos.launder.string(value);
152 |
153 | if (!day.match(/^\d\d\d\d-\d\d-\d\d$/)) {
154 | return null;
155 | }
156 |
157 | return day;
158 | },
159 | async choices() {
160 | const allDates = await query.toDistinct('publishedAt');
161 | const days = [
162 | {
163 | value: null,
164 | label: 'aposBlog:filterAll'
165 | }
166 | ];
167 |
168 | for (const eachDate of allDates) {
169 | if (!days.find((e) => e.value === eachDate)) {
170 | days.push({
171 | value: eachDate,
172 | label: eachDate
173 | });
174 | }
175 | }
176 | days.sort().reverse();
177 |
178 | return days;
179 | }
180 | }
181 | }
182 | };
183 | };
184 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
17 |
18 | **Add blog functionality to ApostropheCMS sites** with article management, date-based filtering, and multiple blog support. Provides both the blog piece type and page templates to get started quickly.
19 |
20 | ## Features
21 |
22 | - **📝 Blog Article Management** - Complete CRUD interface for blog posts with publication dates
23 | - **🗓️ Date-based Filtering** - Built-in year/month/day query filters for archives and navigation
24 | - **🎨 Fully Customizable** — Override templates, add fields, and style it to match your brand
25 | - **🔗 Multiple Blog Types** - Extend to create different blog categories (news, updates, etc.)
26 | - **⏰ Publication Control** - Articles only appear when published date is in the past
27 |
28 | ## Installation
29 |
30 | To install the module, use the command line to run this command in an Apostrophe project's root directory:
31 |
32 | ```bash
33 | npm install @apostrophecms/blog
34 | ```
35 |
36 | ## Usage
37 |
38 | Configure the blog modules in your `app.js` file:
39 |
40 | ```javascript
41 | import apostrophe from 'apostrophe';
42 |
43 | export default apostrophe({
44 | root: import.meta,
45 | shortName: 'my-project',
46 | bundles: [ '@apostrophecms/blog' ],
47 | modules: {
48 | '@apostrophecms/blog': {},
49 | '@apostrophecms/blog-page': {}
50 | }
51 | });
52 | ```
53 |
54 | ### Enable the page type
55 |
56 | Add the blog page type to your page configuration:
57 |
58 | ```javascript
59 | // modules/@apostrophecms/page/index.js
60 | export default {
61 | options: {
62 | types: [
63 | {
64 | name: '@apostrophecms/home-page',
65 | label: 'Home'
66 | },
67 | {
68 | name: '@apostrophecms/blog-page',
69 | label: 'Blog'
70 | }
71 | ]
72 | }
73 | };
74 | ```
75 |
76 | ## Customizing Templates
77 |
78 | The included templates (`index.html`, `show.html`, `filters.html`) are starting points that demonstrate the available data. Override them in your project to implement your own styling and layout:
79 |
80 | ```
81 | modules/
82 | ├── @apostrophecms/
83 | │ └── blog-page/
84 | │ └── views/
85 | │ ├── index.html # Blog listing page
86 | │ ├── show.html # Individual blog post
87 | │ └── filters.html # Date filtering controls
88 | ```
89 |
90 | ## Date-based Filtering
91 |
92 | The blog includes built-in query filters for creating archive navigation and date-based URLs:
93 |
94 | | Filter | Format | Example URL |
95 | |--------|--------|-------------|
96 | | `year` | `YYYY` | `/blog?year=2024` |
97 | | `month` | `YYYY-MM` | `/blog?month=2024-03` |
98 | | `day` | `YYYY-MM-DD` | `/blog?day=2024-03-15` |
99 |
100 | ### Publication Control
101 |
102 | Blog posts use the `publishedAt` field to control visibility. Only articles with publication dates in the past appear on the public site. Editors see all articles in the admin interface.
103 |
104 | > **Note:** This doesn't automatically publish draft changes on the publication date. For scheduled publishing of draft content, consider the [@apostrophecms/scheduled-publishing](https://apostrophecms.com/extensions/scheduled-publishing) module.
105 |
106 | ## Multiple Blog Types
107 |
108 | Sometimes a website needs multiple, distinct types of blog posts. If the blog posts types can be managed together, it might be easiest to [add a new field](https://docs.apostrophecms.org/guide/content-schema.html#using-existing-field-groups) and [query builder](https://docs.apostrophecms.org/reference/module-api/module-overview.html#queries-self-query) to customize blog views. But if the blog posts types should be managed completely separately, it may be better to create separate piece types for each.
109 |
110 | ### Creating a Custom Blog Type
111 |
112 | ```javascript
113 | // modules/news-blog/index.js - for news articles
114 | export default {
115 | extend: '@apostrophecms/blog',
116 | options: {
117 | label: 'News Article',
118 | pluralLabel: 'News Articles'
119 | },
120 | fields: {
121 | add: {
122 | priority: {
123 | type: 'select',
124 | choices: [
125 | { label: 'Standard', value: 'standard' },
126 | { label: 'Breaking', value: 'breaking' },
127 | { label: 'Featured', value: 'featured' }
128 | ]
129 | },
130 | source: {
131 | type: 'string',
132 | label: 'News Source',
133 | help: 'Original source of this news item'
134 | }
135 | },
136 | group: {
137 | basics: { fields: ['priority', 'source'] }
138 | }
139 | }
140 | };
141 | ```
142 |
143 | Every blog piece type needs a corresponding page type that extends `@apostrophecms/blog-page`:
144 |
145 | ```javascript
146 | // modules/news-blog-page/index.js - page type for news articles
147 | export default {
148 | extend: '@apostrophecms/blog-page'
149 | };
150 | ```
151 |
152 | ### Custom Templates for Blog Types
153 |
154 | Each blog type can have its own templates. Create them in the corresponding page module:
155 |
156 | ```
157 | modules/
158 | ├── news-blog-page/
159 | │ └── views/
160 | │ ├── index.html # News listing page
161 | │ ├── show.html # Individual news article
162 | │ └── filters.html # Custom filters for news
163 | └── @apostrophecms/
164 | └── blog-page/
165 | └── views/
166 | ├── index.html # Default blog listing
167 | └── show.html # Default blog post
168 | ```
169 |
170 | This allows you to:
171 | - Style news articles differently from regular blog posts
172 | - Add custom filtering options specific to news content
173 | - Display different fields or layouts for each blog type
174 | - Create distinct navigation and user experiences
175 |
176 | This approach works well when blog types have different:
177 | - **Content structures** - News articles vs. technical tutorials vs. company announcements
178 | - **Editorial workflows** - Different teams managing different content types
179 | - **Display requirements** - Unique styling, filtering, or organization needs
180 | - **URL patterns** - `/blog/`, `/news/`, `/updates/` with distinct navigation
181 |
182 | ## Field Reference
183 |
184 | The blog piece type includes these fields by default:
185 |
186 | - **Title** (`title`) - Article headline
187 | - **Slug** (`slug`) - URL-friendly identifier
188 | - **Publication Date** (`publishedAt`) - Controls public visibility
189 | - **Content** (`body`) - Rich text content area
190 | - **Meta Description** (`metaDescription`) - SEO description
191 | - **Tags** (`tags`) - Taxonomy for categorization
192 |
193 | ---
194 | *Built with ❤️ by the ApostropheCMS team.*
195 | ---
196 | **Found this useful?** [Give us a star on GitHub](https://github.com/apostrophecms/blog) ⭐
--------------------------------------------------------------------------------