Skip to content
V
For developers
Architecture, conventions, the core and security
Modules / Blog

Blog#

The site's editorial content — articles, news, events, galleries, carousels, a guestbook and a contact form. The code lives in app/UI/Api/Blog/, the front end in app/UI/Front/Blog/.

The blog is also the system's reference module: it was the first content wired into Elasticsearch and its front presenter is the model for splitting into traits. Anyone looking for how something is done properly in this CMS starts here.

What the module is made of#

Group Entities Note
content Article, News, Event, Intro plus their *Text each has its own category and listing
media Gallery + GalleryAssetRel, Carousel + CarouselItem
taxonomy Category (a tree), Tag (a flat list)
interaction Guestbook + GuestbookMessage, ContactMessage, ArticleDiscussionMessage
extras FlashMessage, ArticlePollRel, ArticleAdvertRel links to polls and advertising

Two models to copy from#

Model File Why this one
an Elasticsearch source app/UI/Api/Blog/Models/Search/ArticleDocumentSource.php the article was the first content wired in; there are four sources today and all derive from it
splitting a presenter into traits app/UI/Front/Blog/Presenters/Traits/ — Inits/, Lists/, Details/ one of the few presenters where the split follows the convention

In detail: Elasticsearch and Code conventions.

Do not copy patterns from the guestbook

It is the oldest part of the module and some of its code is still in the previous generation's shape. It looks like a valid model because it is the same module — but it is not.

Categories and tags#

Categories form a tree and are reflected in URLs; tags are a flat list and only label.

A tag is cheaper than a category

A category changes the structure and the URLs — and with them the translations, the sitemaps and inbound links. When labelling is enough, use a tag.

Blog categories have a GLOBALLY unique href

cms_mod_blog_category_texts and cms_mod_blog_article_category_texts carry UNIQUE(href) — without the language. The other category tables in the system correctly have UNIQUE(href, language_id). Two categories in different languages therefore cannot share an address here, and Czech and Slovak share plenty of words (doprava, zahrada). It surfaces only when the second language is created, as Duplicate entry 'doprava' for key 'ind_href' — a collision check before the insert therefore has to run without a language filter.

Articles, news and events are three separate pages (ids 50, 52, 54) with three presenters, but the category is shared. The target of a category link is therefore decided by app/UI/Front/Blog/Components/Traits/BlogListTargetTrait.php:

Method What it does
blogListTarget($type) by item type (article, news, event) returns the target and the page id
blogListTargetForPage($presenter) the same by the current page (52/53 = news, 54/55 = events, otherwise articles)
blogListParams($slug, $categoryId, $regionId) the link parameters; with a region selected the category segment is always explicit (- = none), otherwise the region would shift into the category's place
blogRegionLinks($presenter, $type, $items, $dispatcher) links /<listing>/-/<region> to a listing of the same type filtered by the item's region

The category parameter is categoryId (route mask 101/102/104), not category. Before, category links always led to articles (and produced /clanky/o-1?category=109), and the numeric address /udalosti/1 fell on a TypeError in CategoryTrait. Now:

  • EventListRouteFactory registers the translation under the key categoryId, NewsListRouteFactory has it uncommented,
  • CategoryTrait casts the id to int; an unknown numeric id is canonicalised by a 301 to the slug,
  • cards, the news detail, CategoryList, CategoryChildrenList and breadcrumbs call BlogListTargetTrait.

Links without categoryId remain

A few places nobody includes (Article/parts/*, BreadcrumbComponent/CategoryTrailTrait always leads to articles) and the old blog CategoryList still count on it. The module switcher in template1/…/shared/main.latte adds ?category=<id> to the Blog link, which Blog ignores.

Region on articles and news#

Only Event used to have a region (a column, a Picker in the administration, a filter including the subtree, the route /udalosti/<cat>/<region>/o-<page>). Article and News got it by the same pattern.

Layer What
SQL sql/2026-10-01_01_blog_article_news_region.sql: the column cms_system_regions_region_id on articles and news, an index and a foreign key ON DELETE SET NULL (rollback …_91_). Existing records stay without a region
Entities in four layers (Article, News) — an association region + a scalar regionId
Administration RegionPickerTrait in the presenters, the hidden regionId is filled by the Picker; in Article/News/EventFormTrait both setRegion() and setRegionId() are set on save
Front Blog\RegionTrait (initCurrentRegion(), prepareRegionFilter()), the filter region.lft ≥ … ∧ region.rgt ≤ … including the subtree
Route the regionId translation in ArticleListRouteFactory and NewsListRouteFactory (the masks already had the segment)
Listing include region.translations in listings, details and in ArticleListProvider, TaggedArticleList, NewsComponent

The region could not be cleared from the form until setRegionId() was set too

extract sends the old scalar regionId of the loaded entity to the Api. Clearing the choice (setRegion(null)) without setRegionId(null) therefore came back on save. A new form with a region must set both.

A record without a region is not shown when a region is selected

It is deliberate (the same as for the category); the filter is top-down. An unknown region slug returns the listing without a filter, like for a category.

The region name on an item: the row (Style_1) and table (Style_3) listings and the details (info-row) show it with a link; the card styles with badges (Style_2/4/5/6) deliberately leave it out, the badge row gets trimmed. ArticleDocumentSource does not index the region into Elasticsearch — the filter goes through the database, as with classifieds.

Multilingual content#

The texts live in a child entity, one per language (ArticleText, NewsText, …).

An empty translation beats the fallback

A stored empty string is a valid value, so it wins over the fallback and the element comes out with no name. Either translate it or do not create the row at all — in detail Translations.

A text entity has a derived identity

It has no key of its own — it is composed of the parent and the language. A missing use ORM in such an entity surfaces silently: the mapping is not loaded and the entity behaves as if it had no columns.

Comments, ratings and polls#

The blog does not handle them itself — they are shared components from the Base module (Comment, CommentThread, Rating, RatingThread, Poll).

Comments exist for articles, galleries, the guestbook and events. An event has the thread type ThreadType::BlogEvent ('blog_event'): the Api EventManager creates its comment thread on save (CommentThreadCreatorTrait), and an older event without a thread gets one on its next save; an ordinary edit of an event that has a thread creates nothing. Deleting an event deletes its thread too (OwnedThreadDeleteTrait).

Changing a shared component affects three other modules

Comments and ratings are used, besides the blog, by the e-shop (products), classifieds and the company catalogue. A fix "just for articles" shows up everywhere — and only gets tested by whoever remembers to.

User-entered text is an XSS trap

addSimpleEditor() sanitises nothing by itself. Rich text has to be purified in the Api beforeSave, plain text is printed through |breaklines. In detail: Forms.

The rating component on a page#

Rating (app/UI/Front/Base/Components/RatingComponent/) rates anything that holds a scalar ratingThreadId; the thread must exist. The look is set by the style, either with setStyle() in createComponentRating() or with a variant in the template ({control rating:review}, {control frontBaseRating:number $id}).

Style Variant What it renders
STYLE_CLASSIC (default) {control rating} interactive stars, one click = one rating via AJAX
STYLE_STATIC :static read-only average
STYLE_REVIEW :review summary bars, review button and review list
STYLE_SUMMARY :summary summary bars without reviews
STYLE_REVIEWS :reviews review list only
STYLE_REVIEWS_DETAILED :reviewsDetailed summary, star filter, sorting and the list; setShowSummary(false) turns the summary off
STYLE_NUMBER :number the average as a plain number, no HTML
STYLE_THUMBS :thumbs thumb up (value 5) and down (value 1); setThumbs(['up']) keeps only "like"

Who may rate is decided by the caller, not by the component:

Setting What it controls
setAllowAnonymous() whether a guest may rate (article yes, profile and product no)
setAllowVoting() whether this visitor may write a review (e-shop: buyers of the set only, CheckoutManager::setBuyers())
setVerifiedBuyersResolver() the "Verified purchase" badge on other people's reviews

The resolver is a list<int> → list<int> function: the component calls it once with all authors on the page and gets back the verified ones. The component is shared and knows nothing about orders; without a resolver (blog, profile) there is no badge. It does not decide who may write a review.

The reviewsDetailed style already contains the summary

On a page with STYLE_REVIEWS_DETAILED do not add STYLE_SUMMARY as well, otherwise the same numbers are printed twice.

Rating summary#

Listings do not read individual ratings but the summary on the thread (RatingThread): rating_count, rating_avg and count_1 to count_5, computed only from ratings with active = 1.

Who recalculates the summary When
RatingManager::afterSave() → recalcThread() after every rating save, only its thread
RatingManager::afterDelete() after a rating delete (also a batch delete([…])), the threads of the deleted ratings, in the same transaction
RatingManager::deleteAll() after a bulk delete by filters, the affected threads
RatingManager::updateColumns() after a change of approved, deleted, value or threadId, the threads before and after the change; other columns (for example userName) do not trigger it
cron /cron/base/rating/recalc → recalcAllThreads() daily; one set-based UPDATE over all threads, a safety net against drift; a thread with no active ratings is reset to 0

Raw SQL does not recalculate the summary

A delete and a column write through the manager recalculate the summary right away. When you change ratings with your own SQL outside the manager, call RatingManager::recalcThread($threadId) afterwards, otherwise only the nightly cron fixes it.

One vote per visitor#

Repeated ratings and votes on the usefulness of a review are guarded by DuplicateVoterGuardTrait::voterFilters() (RatingManager::hasRated(), RatingVoteManager::hasVoted()):

Who votes How the previous vote is looked up
logged in userId, the IP is ignored (one account = one vote from any device)
anonymous userId IS NULL AND ip = …
anonymous without an IP cannot be decided, the method returns null

A unique index does not guard an anonymous voter

A UNIQUE(…, user_id) index does not treat two NULL values as equal, so without an application check an anonymous visitor could vote again and again. A new voting entity needs the same check in the Api and must read null from voterFilters() as "cannot verify", not as "has not voted".

Approval of a new comment or rating#

Whether a new comment or rating is published right away is decided by the Api manager's beforeSave() from the Base module settings (module 2). In the administration they are under Site settings → the section Komentáře, hodnocení a newsletter (comments, ratings and newsletter).

Key Applies to When the row is missing
comment.useAutoApproval every new comment (CommentManager) 1 = approved right away
rating.useAutoApprovalStars a stars-only rating (RatingManager) 0 = waits for approval
rating.useAutoApprovalReviews a written review — text, pros, cons (Rating::isReview()) 0 = waits for approval

The front rating component may override both rating keys for one instance (setOverrideRatingUseAutoApprovalStars(), setOverrideRatingUseAutoApprovalReview()); the value goes into $meta under the same key and $meta wins over the database. Editing an existing record does not touch the approval state — otherwise every save from the administration would overwrite a manual approval, unapproval or soft delete.

Comments and ratings have opposite defaults

Comments have always been published right away, so a missing row means 1. Unifying the default to 0 would silently switch on moderation for every module and comments would vanish from the site after submit. Moderated mode is switched on by storing 0 in the row.

The review key is plural

rating.useAutoApprovalReviews, like …Stars. Reading …Review would never find the row and reviews would always go to moderation, whatever the switch says.

Insert gates#

The checks run in the Api manager's beforeSave(), because every insert passes through it — the front, the administration through ApiBridge and the external :Api:Base:*:save. A form may show its own message on top, but the manager gate is the one that counts.

Manager Check Response
CommentManager content creation quota (base-comment) RATE_LIMIT_EXCEEDED
CommentManager the parent of a reply belongs to the same thread (rejectForeignParent(), one query on the pair id + threadId) INVALID_VALUE, errorCode: commentParentThreadMismatch
RatingManager the rating thread exists INVALID_DATA
RatingManager a logged-in author is not the owner of the rated object INVALID_DATA, errorCode: selfRating
RatingManager the same visitor has not rated here yet (hasRated(), see above) DB_DUPLICATE_ENTRY, errorCode: alreadyRated

A comment whose parent is in another thread vanishes from the tree

threadId and parentId are two independent scalars. The tree is loaded by threadId only, so the parent is missing from the loaded set and the reply is rendered nowhere, although it is in the database. A new UI that creates replies must therefore not bypass the manager gate.

Reply to a rating: the author and the rated party#

A rating has two different people who may do something with it, and each has its own action.

Who Where it comes from May Action and role
the review author Rating::$userId (RatingManager::isOwnedBy()) edit their review :Api:Base:Rating:save, role owner
the owner of the rated object from the DB: rating → thread → object → owner (RatingManager::findRespondableOwner()) write or delete the reply (response) :Api:Base:Rating:saveReply, role receiver

Only RatingManager::saveReply() writes the reply; responseDatetime is set by the server and cleared when the reply is deleted. The front asks internalActionCanRespondTo() beforehand; the answer is in data.canRespond and any other shape reads as false.

The generic save is wrapped (RatingPresenter::internalActionSave()): when the payload changes the reply compared with the DB state (RatingManager::payloadChangesReply()), the caller must also pass the saveReply guard. Without it the author could write a "seller reply" into their own review through save.

save payload payloadChangesReply()
no response key false
a new rating with an empty reply (the front sends response => null) false, no database query
a new rating with a reply true
an update where the normalised reply differs from the stored one (deletion too) true
an unknown id true — the guard decides

Today the owner can be derived only for a user profile (ThreadType::SystemUser, link User::$ratingThreadId). For other types (article, product, catalogue advert…) findRespondableOwner() returns null and nobody but an administrator may reply.

Procedure: allow replies for another type of rated object

  1. Extend RatingManager::findThreadOwner() with a branch for the new ThreadType: find the object by its ratingThreadId column and return its owner (User).
  2. Change nothing else. canRespondTo(), the saveReply guard and the save wrapper ask the same method, so the rule applies everywhere at once. The same method also stops the owner from rating their own object (selfRating).
  3. Verify the owner's reply, the denial of the review author and the denial of a stranger (RatingSaveReplyReceiverGateTest is the model).

Do not confuse isOwnedBy() and canRespondTo()

A naive rating.userId === jwt.userId for a reply gives exactly the opposite result: the review author could reply to themselves and the rated party could not.

Do not call saveReply() outside the guarded action

The manager does not verify ownership again — the presenter guard, the only place that knows the identity from the JWT, has decided. A call from anywhere else bypasses authorisation.

Without the saveReply privilege row nobody can reply

When the key :Api:Base:Rating:saveReply is missing in the database, the external path denies everyone, the administrator included. The emergency mode internalMode: off lets the front through, so the fault shows only after switching to enforce.

Usefulness votes and review reports#

"Was this review helpful?" and "Report" are written by RatingVoteManager into one table (RatingVote, told apart by kind). Only the internal RatingPresenter actions are called — vote, report and myVotes (how the visitor voted on the reviews on the page). They have no action*(), and a CRUD over RatingVote deliberately does not exist: a generic save would accept a foreign userId and any ip and bypass the duplicate check.

Situation Response
the rating does not exist or is not active (unapproved, deleted) ITEM_NOT_FOUND
the author votes on their own review PERMISSION_DENIED
anonymous and rating.allowAnonymousReviewVote is not 1 (a missing row means "no") PERMISSION_DENIED
a repeated vote or report DB_DUPLICATE_ENTRY — rejected, not overwritten
success the row is stored and the count on the rating is incremented in the DB (incrementCount()), not overwritten

A report neither hides nor deletes the review; it only raises reports and the administrator filters by it. The caller must take userId and ip from the server-side identity (front: session and getRemoteAddress()), never from the payload.

Do not add a unique key on (rating_id, ip)

Logged-in users are covered by UNIQUE uq_rating_user, also against concurrent requests; anonymous ones are guarded by the application by IP. A unique key on the IP would block two logged-in users from the same network.

Poll vote#

PollPresenter::internalActionRegisterVote() (externally actionRegisterVote()) hydrates the whole PollVote from the payload, but always overwrites ip and insertDatetime with server values before hydration. The IP is part of the primary key and the only protection against repeated voting.

#[ExternalReadonly] guards nothing in a custom action

The attribute strip runs only in SaveTrait::saveEntityData() (the generic save). A custom action that hydrates an entity from the payload must overwrite server-computed fields itself — otherwise the caller sends voteData[ip] and votes again and again.

Comments and the query cache#

Reading comments does not depend on the caller, so CommentPresenter has the query cache enabled like the other presenters and comment listings do not hit the database every time.

Do not put private communication among comments

The read result would start to depend on the caller, and the wire-payload cache is user-agnostic: an administrator would warm it and a guest would read a foreign conversation, or the other way round. isQueryCacheEnabled(): false, filterReadPayload() and filters in managers and presenters would all have to come back. Private communication gets its own table (models: work item comments in My company, Private messages).

Comments and ratings in an object's edit form#

The comments or ratings tab in the edit form of an article, product or advert is rendered by the shared admin components CommentThread and RatingThread. The component is keyed by the thread id, so any edit form that owns a thread can embed it. Adding and editing run in the shared admin modal window, deleting is soft (the deleted flag).

The thread travels in the form as the hidden field threadId. The client can rewrite that field, so it is only an echo: which thread gets written is decided by the server-side context the host passes to the component when creating it.

Where What is checked On mismatch
signals add…!, edit…!, toggleDeleted! threadId from the URL = host context 403
form submit hidden threadId = context; the reply parent and the edited record are looked up by id + threadId nothing is saved, the modal shows an error
no context nothing the component saves nothing and opens no modal

Procedure: adding comments or ratings to another edit form

  1. Inject the factory (CommentThreadFactory, RatingThreadFactory) through the constructor of the presenter or the edit component.
  2. Set the context in the component factory — a callback returning the thread of the entity being edited:

    protected function createComponentCommentThread(): CommentThread
    {
        return $this->commentThreadFactory->create()
            ->setThreadContext(fn(): ?int => $this->editedArticle?->getCommentThreadId());
    }
    

    The callback runs only at signal or submit time, when the host's action*() has already loaded the entity. 3. Put {control commentThread $commentThreadId} ({control ratingThread $ratingThreadId}) in the tab template; pass the thread id from render*(). 4. Check privileges: the components guard :Admin:Base:Comment:edit / :del and :Admin:Base:Rating:edit / :del themselves, also for a hand-built URL.

Without setThreadContext() the component silently does nothing

The list renders, but the Add and Edit buttons open no modal (the signal ends with 403) and saving writes nothing. The context guards against a forged threadId writing a comment or rating to someone else's object.

Moderation across sections. Next to the tabs there are two central lists over the whole tables: CommentPresenter and RatingPresenter (thread detail CommentThreadPresenter, RatingThreadPresenter). The section (e-shop, blog…) is stored on the thread as type, not on the comment row. An association to the thread cannot be mapped, because the scalar threadId already maps that column and Doctrine rejects a second writable mapping. The list therefore loads the threads separately and pairs them through a threadId => type map; a module's menu link is the same list with type prefilled. The list cannot show the concrete article or product: the object holds the link (commentThreadId, ratingThreadId), not the thread.

The section map loads every thread

A thread exists for every commentable object, so with a large catalogue the list slows down. It then needs to fetch types only for the threadIds on the current page. Threads without a type (the column is nullable) have an empty section and the section filter does not find them.

Article lists on the front end#

The article boxes on the home page and in the side columns are thin wrappers over ArticleListProvider. A wrapper only sets the order (in its template as $orderBy), maybe an extra filter, and passes them on; loading and rendering is done by the provider.

Component Order Extra filter Default style, count
LastArticleList published ↓ — Row, 5
MostReadArticleList views ↓ — Row, 5
MostCommentedArticleList commentThread.commentCount ↓, published ↓ — Line, 3
TopRatedArticleList ratingThread.ratingAvg ↓, ratingThread.ratingCount ↓ — Row, 5
MainArticleList published ↓ main = 1 Editorial, 1
TaggedArticleList id ↓ tags.id IN (…) Cards, 5; its own loading, not through the provider

Comment and rating counts are not aggregated: the shared threads hold them (cms_mod_base_comment_threads.comment_count, cms_mod_base_rating_threads.rating_avg and rating_count), which the article points to through a nullable FK. The order goes through the association, which JoinHelper joins with a LEFT JOIN, so an article without a thread does not drop out of the list, it only falls to the end. The second key for ratings is intentional: without it an article with a single five would outrank an article with a hundred ratings averaging 4.9.

The provider always adds activated = 1 and published <= NOW to the caller's filters. It assigns them after the caller's filters under the keys activated and published, so the caller cannot override them, not even with the same key. Filter objects are built in the component's PHP, not in Latte (MainArticleList hands $filters to its template ready-made).

Procedure: add an article box with its own order#

  1. Copy the nearest wrapper (LastArticleList for a plain order, MainArticleList when you need a filter).
  2. Set $orderBy in the wrapper's template; write an order through a thread as a dotted path (ratingThread.ratingAvg), not as a scalar.
  3. Build the extra filter in render() and pass it as $filters under a key that does not clash with activated and published.
  4. Check the provider's include: the Line style prints the comment count from commentThread; without it there would be nothing to print.

Without a marked main article the lead block is empty

With no article carrying the main flag, MainArticleList renders the "no articles" message and does not substitute the newest article. This is intentional: the editors must notice that they picked no headline anywhere. Several articles may carry the flag; the caller's order and limit pick among them.

TaggedArticleList with empty tags lists all articles

Empty $tagIds add no tag filter at all and the component behaves as a plain list of the newest (id ↓). A tag without articles, on the other hand, gives an empty list with the usual message.

showOnlyCategoryIds limits the list to categories

All wrappers and ArticleListProvider::render() pass it to InitRenderTrait::initRender(), which builds a categories.id IN (...) filter from it (same shape as tags.id in TaggedArticleList). An article needs to be in one of the categories. An empty list adds no filter and the list shows all categories. The parameter overrides a categoryId key in $filters.

The gallery on the home page is drawn by LastGallery: it picks the newest active and published gallery (or the given $galleryId) and hands rendering over to frontSystemGallery. It skips galleries with only_for_logged, even when their id is given, because on a public page half of the visitors could not reach them. Photos above $maxAssets are summed into "+ N".

A carousel from the administration (Blog → Carousels) is drawn on the website by the component frontBlogCarousel (app/UI/Front/Blog/Components/CarouselComponent/):

{control frontBlogCarousel 1}      {* carousel 1, one slide per view *}
{control frontBlogCarousel 1, 3}   {* three slides per view *}
Rule How
which carousel only an active one (activated = 1) with an empty or past publication time
slides the slide's HTML code in the page language; a slide without code in that language is skipped, without slides nothing is drawn
skeleton the shared .list--carousel (like flash messages); arrows, dots and the step come from the same script, the number of visible slides is carried by --carousel-cols
arrows and dots only when there are more slides than visible ones ($perPage)
permissions reading :Api:Blog:Carousel:getAll is granted to guests and logged-in users, like flash messages

The slide code is printed unescaped

A slide is HTML from the administration and the template prints it with |noescape. Writing slides must therefore not be granted to roles outside the administration; a new write path from the front or an external client would need purification in the Api beforeSave().

Content images#

Images do not travel through the object graph. The administration sends them in $meta and the Api manager processes them in afterSave(): it stores the Asset with its variants, writes the link and releases the images that dropped out of the form (see Image variants).

Entity Keys in $meta Missing key
Article, Category, Event, FlashMessage (one image) image = {name, data} (base64) → a new image; imageDelete = true without image → the asset foreign key is cleared the image stays
Gallery images = a list of rows in gallery order; a row carries image (new) or assetId (kept) and texts[<langId>] = {title, description} without the images key the gallery stays as it is
Intro (one image per language) imageLanguages = ids of the form languages; for each image_<langId> (new), imageDelete_<langId>, imageCurrentId_<langId> (kept) without imageLanguages the links stay as they are

The gallery and the intro panel write their links through updateColumn('assetRels', …), that is by full replacement. Whatever is not in $meta disappears from the gallery. An external client may send only the gallery images (nested in data, $graphDataKeys); the other keys live only in $meta, which the external path drops.

Procedure: your own write of gallery or panel images

  1. Load the current images and send all of them: rows with assetId for a gallery, imageCurrentId_<langId> for every language in imageLanguages for a panel.
  2. For a kept gallery image send texts too, otherwise it loses its captions.
  3. The order of gallery rows is the order of images; an empty row and a repeated image are skipped.
  4. Check the links in the database (cms_mod_blog_gallery_image_relations, cms_mod_blog_intro_image_relations) and that the dropped images are gone from cms_system_images.

A language in imageLanguages without an image loses its image

When neither a new image nor imageCurrentId_<langId> comes for a language, no link is written and the image is released, files included. The same happens to a gallery image missing from images.

Gallery image captions are rewritten in full

The texts are written by delete and insert (updateColumn('translations', …)) for every row, kept ones included. A row without texts deletes the captions in all languages. A language with an empty title and description is skipped.

Scheduled tasks#

Endpoint What it does Recommended interval
/cron/blog/elastic/sync incremental index synchronisation every 10 min
/cron/blog/elastic/reindex a full reindex nightly
/cron/blog/sitemap/{article,event,news} content sitemaps nightly
/cron/blog/sitemap/{article,event,news}-category category sitemaps nightly

Where to look#

I want Where
the model search source app/UI/Api/Blog/Models/Search/ArticleDocumentSource.php
the model presenter split app/UI/Front/Blog/Presenters/Traits/
the content managers app/UI/Api/Blog/Models/Managers/
the shared comments and ratings app/UI/Api/Base/Models/Entities/
approval, insert gates, the reply to a rating app/UI/Api/Base/Models/Managers/{Comment,Rating}Manager.php, app/UI/Api/Base/Presenters/RatingPresenter.php
usefulness votes and reports app/UI/Api/Base/Models/Managers/RatingVoteManager.php
the cron tasks app/UI/Cron/Blog/Presenters/
the target of category and region links app/UI/Front/Blog/Components/Traits/BlogListTargetTrait.php
the region on the front app/UI/Front/Blog/Presenters/Traits/Inits/RegionTrait.php
article boxes and the gallery on the home page app/UI/Front/Blog/Components/{ArticleListProvider,MainArticleList,TaggedArticleList,LastGallery}Component/

Follow-up chapters: Elasticsearch · Discussion · Translations · Code conventions

Cookie settings