Publicaciones y Feed — Referencia técnica
← Volver a Publicaciones y Feed
Dónde vive esto
Backend
apps/backend/graphql/resolvers/post.resolver.js—feed,trendingPosts,userPosts,postStats,createPost,updatePost,deletePost,incrementPostViewsapps/backend/graphql/resolvers/post-interaction.resolver.js—likePost,unlikePost,postInteractionCounts,hasUserLikedPostapps/backend/graphql/resolvers/post-comment.resolver.js—createComment,postComments,commentReplies, reacciones a comentariosapps/backend/graphql/types/post.type.js— tipos SDL dePost/PostMediaapps/backend/managers/post-managers/post.manager.js— ensamblaje del feed, manejo de borradores/publicación, lógica de deduplicación del conteo de vistas
Frontend
apps/frontend-nextjs/src/page-components/HomePage.tsx— página principal del feedapps/frontend-nextjs/src/app/home/page.tsx— entrada de la ruta/homeapps/frontend-nextjs/src/components/PostCard.tsx— renderiza una sola publicación en el feedapps/frontend-nextjs/src/components/CreatePostModal.tsx— modal de creación de publicaciones con carga de contenido multimedia
Checklist de implementación técnica
-
feed— resolver conectado enpost.resolver.js; se llama desdeHomePage.tsxvíauseQueryde Apollo -
reactToComment(commentId, reaction)/removeCommentReaction+PostComment.viewerReaction/reactionCounts— reacciones a comentarios (like/love/haha/wow/sad/angry). Se corrigió un error de persistencia:post-comment-interaction.access-service.jsconsultabacomment_iden lugar depost_comment_id -
likePost/unlikePost— resolvers conectados enpost-interaction.resolver.js; se llaman desdehandleLikedePostCard.tsx(~línea 499) -
createComment— resolver conectado enpost-comment.resolver.js; se llama desdePostModal.tsxystories/StoryViewer.tsx -
repostPost/undoRepost— resolvers conectados enpost.resolver.js; se llaman desdehandleRepostdePostCard.tsx(~línea 566) yhandleRepostdePostModal.tsx(~línea 710), con una verificaciónhasUserReposted -
sharePost— no existe ninguna mutation de GraphQL con ese nombre en resolvers/schema. El ícono de "compartir" enPostCard.tsxen realidad ejecuta la acción de repost; existe por separado una función nativa real (no GraphQL) de compartir/copiar enlace enPostOptionsMenu.tsx(navigator.share/ copiar enlace, sin necesidad de llamada al backend) -
createPost— resolver conectado enpost.resolver.js; se llama desdeCreatePostModal.tsx, que gestiona la carga de contenido multimedia, la extracción de hashtags y el envío de usuarios etiquetados -
taggedUsers(PostMentiona través depost_mentions) —handleImageTapForTagdeCreatePostModal.tsx(~línea 751) permite al usuario tocar un punto en una imagen subida, buscar un usuario (SEARCH_USERS_FOR_TAG), y los pines se envían comotaggedUsersencreatePost(~línea 907);post.resolver.js:478los expone de vuelta en el tipoPost.PostTagInput.mediaIndex/x/yahora son opcionales, por lo que un usuario también puede ser etiquetado sin un pin en la foto (por ejemplo, en una publicación de solo texto) — esas etiquetas "sin posición" se combinan con las etiquetadas por pin antes del envío. - Extracción automática de hashtags —
post.manager.js:217llama ahashtagManager.extractAndCreateHashtagsen cadacreatePost, lo que alimenta el feed/hashtag/[tag](ver Hashtags y Tendencias) -
feed(reutilizada para la pestaña "Descubrir") — la queryDISCOVER_POSTSdeExplorePage.tsxusa un alias del campofeed -
trendingPosts— se agregó lógica de ranking real enpost.access-service.js#getTrendingPosts(publicaciones públicas recientes puntuadas porviewsCount*1 + likesCount*3 + commentsCount*4 + sharesCount*5 + savesCount*4en una ventana de 48h por defecto) y se expuso a través depost.resolver.js; conectada en la pestaña "En tendencia" deExplorePage.tsx. Antes era solo un comentario de documentación (@method getTrendingPosts) sin implementación en ningún lugar y sin campo alguno en el schema. -
Post.viewerHasLiked/viewerHasSaved/viewerHasReposted/interactionCount/giftStats— resueltos del lado del servidor como resolvers de campo dePostenpost.resolver.js, incluidos en cualquier query que ya cargue la publicación (feed,userPosts,post). La query del feed deHomePage.tsxlos selecciona, yPostCard.tsxlos lee directamente de la publicación (su verificacióninlineState) en lugar de disparar queries separadas dehasUserLikedPost/isPostSaved/hasUserReposted/postInteractionCountspor cada tarjeta — esas queries por publicación se mantienen solo como respaldo para contextos que no precargan este estado (por ejemplo,PostModal.tsx). -
ContentTagManager/UserContentInterest— la lógica existe encontent-tag.manager.js, pero no es requerida porpost.manager.jsni por ningún resolver — código huérfano, sin exposición en el schema ni uso en el frontend
Modelo Post
| Campo | Tipo | Descripción |
|---|---|---|
text | String | Texto de la publicación |
visibility | Enum | public / followers / private / subscribers |
location | String | Etiqueta de ubicación opcional |
media | [PostMedia] | Imágenes o videos adjuntos |
viewsCount | Int | Conteo acumulado de vistas |
sharesCount | Int | Conteo de veces compartida |
isPublished | Boolean | Borrador vs. publicada |
Contenido multimedia (PostMedia)
Cada elemento multimedia tiene: mediaUrl, mediaType, thumbnailUrl, duration, width, height, aspectRatio, order. El campo order controla el orden de visualización en publicaciones con múltiples elementos multimedia. filterCss incluye un filtro estilo Instagram (por ejemplo, contrast(1.2) saturate(1.35)) elegido al momento de publicar, aplicado sobre la imagen original al momento de mostrarla. objectFit (cover/contain) controla cómo la imagen llena su marco. isPreview marca un elemento como vista previa gratuita en una publicación paga — visible para todos incluso antes de la compra, mientras el resto permanece bloqueado.
Queries
feed devuelve el feed principal personalizado del usuario autenticado — publicaciones de las cuentas que sigue, ordenadas por relevancia y actualidad. Admite paginación basada en cursor mediante limit y offset. Una publicación que el usuario haya publicado él mismo en los últimos 5 minutos se fija en la parte superior de su propio feed (post.access-service.js#getFeed), de modo que una publicación recién creada aparece de inmediato en lugar de esperar a ganar puntaje de interacción/actualidad.
userPosts obtiene todas las publicaciones publicadas de un usuario específico. Útil para renderizar la cuadrícula de un perfil. Respeta la configuración de privacidad del usuario objetivo — las cuentas privadas solo devuelven publicaciones si quien llama es seguidor.
postStats devuelve contadores agregados de una publicación sin obtener el objeto completo. Úsalo para actualizar estadísticas de forma ligera sin volver a cargar el contenido multimedia ni los comentarios.
query GetFeed($limit: Int, $offset: Int) {
feed(limit: $limit, offset: $offset) {
id text visibility location viewsCount sharesCount
# Bundled per-viewer state - lets the card skip a separate query per post
viewerHasLiked viewerHasSaved viewerHasReposted interactionCount
user { id username profilePicture }
media { mediaUrl mediaType thumbnailUrl aspectRatio }
comments { id text user { username } }
interactions { reactionType user { username } }
}
}
# Recent public posts ranked by engagement - not personalized, unlike feed
query GetTrendingPosts($limit: Int, $offset: Int, $windowHours: Int) {
trendingPosts(limit: $limit, offset: $offset, windowHours: $windowHours) {
id text viewsCount likesCount commentsCount sharesCount
user { id username profilePicture }
media { mediaUrl mediaType thumbnailUrl }
}
}
# All posts from a specific user's profile
query UserPosts($userId: ID!, $limit: Int, $offset: Int) {
userPosts(userId: $userId, limit: $limit, offset: $offset) { id text media { mediaUrl mediaType } }
}
# Lightweight counters for a post
query PostStats($postId: ID!) {
postStats(postId: $postId) {
viewsCount likesCount commentsCount sharesCount
}
}
Mutations
createPost crea una nueva publicación. Si isPublished es false la publicación se guarda como borrador y no se muestra en los feeds. El contenido multimedia se puede pasar como mediaUrls (arreglo simple de URLs) o mediaItems (URL + dimensiones).
updatePost permite editar el texto, la visibilidad o la ubicación de una publicación existente. Solo el autor de la publicación puede actualizarla.
deletePost elimina permanentemente la publicación y todo su contenido multimedia, comentarios y reacciones asociados.
incrementPostViews debe llamarse una vez por vista única — típicamente cuando la publicación entra en el viewport. El servidor deduplica las llamadas rápidas.
mutation CreatePost($input: PostCreateInput!) {
createPost(input: $input) { id text visibility isPublished }
}
mutation UpdatePost($postId: ID!, $input: PostUpdateInput!) {
updatePost(postId: $postId, input: $input) { id text visibility }
}
# Permanently deletes post and all associated data
mutation DeletePost($postId: ID!) { deletePost(postId: $postId) }
# Call once when a post enters the user's viewport
mutation IncrementViews($postId: ID!) { incrementPostViews(postId: $postId) { viewsCount } }
Reacciones (6 tipos)
Los usuarios pueden reaccionar a una publicación con una de 6 emociones. Llamar a likePost nuevamente con un interactionType distinto reemplaza la reacción anterior — un usuario solo puede tener una reacción por publicación a la vez.
enum InteractionType { like love haha wow sad angry }
# Add or change your reaction to a post
mutation LikePost($postId: ID!, $type: InteractionType) {
likePost(postId: $postId, interactionType: $type) { id reactionType }
}
# Remove your reaction entirely
mutation UnlikePost($postId: ID!) { unlikePost(postId: $postId) }
# Get a breakdown of reactions by type
query InteractionCounts($postId: ID!) {
postInteractionCounts(postId: $postId) {
like love haha wow sad angry total
}
}
# Check if the current user has already reacted
query HasUserLiked($postId: ID!) { hasUserLikedPost(postId: $postId) }
Comentarios (anidados)
Los comentarios admiten un nivel de anidación mediante parentId — pasa un parentId para crear una respuesta a un comentario existente. Omítelo para crear un comentario de nivel superior.
postComments devuelve los comentarios de nivel superior con sus respuestas precargadas. Para publicaciones con muchos comentarios, usa commentReplies para cargar las respuestas de forma diferida bajo demanda.
# Create a comment; pass parentId to reply to an existing comment
mutation CreateComment($input: PostCommentCreateInput!) {
createComment(input: $input) { id text parentId user { username } }
}
mutation UpdateComment($commentId: ID!, $text: String!) {
updateComment(commentId: $commentId, input: { text: $text }) { id text }
}
mutation DeleteComment($commentId: ID!) { deleteComment(commentId: $commentId) }
# Top-level comments with nested replies
query PostComments($postId: ID!, $limit: Int, $offset: Int) {
postComments(postId: $postId, limit: $limit, offset: $offset) {
id text
user { username profilePicture }
replies { id text user { username } }
}
}
# Lazy-load replies for a specific comment
query CommentReplies($commentId: ID!, $limit: Int, $offset: Int) {
commentReplies(commentId: $commentId, limit: $limit, offset: $offset) { id text user { username } }
}
# Cheap counter — use before fetching comments to decide whether to show the section
query CommentCount($postId: ID!) { postCommentCount(postId: $postId) }
Reacciones a comentarios
Los comentarios admiten los mismos 6 tipos de reacción que las publicaciones. Cada usuario puede tener como máximo una reacción por comentario.
# React to a comment (replaces any previous reaction); returns a plain Boolean
mutation ReactToComment($commentId: ID!, $reaction: String!) {
reactToComment(commentId: $commentId, reaction: $reaction)
}
# Remove your reaction from a comment; also a plain Boolean
mutation RemoveCommentReaction($commentId: ID!) {
removeCommentReaction(commentId: $commentId)
}
# There is no standalone "commentReactions" query - per-type counts and the
# viewer's own reaction come back as fields on the comment itself.
query CommentWithReactions($id: ID!) {
postComment(id: $id) {
id
reactionCounts
viewerReaction
}
}
Etiquetas de contenido y personalización del feed
El backend clasifica automáticamente las publicaciones con etiquetas ContentTag usando Google Cloud Vision / Video Intelligence. Estas etiquetas impulsan el motor de personalización del feed.
UserContentInterest registra señales de interacción por etiqueta para cada usuario: postsCreated, postsLiked, postsCommented, postsShared, postsSaved, postsViewed, timeSpentSeconds. A partir de estas señales, el sistema deriva un interestScore, un affinityLevel (low / medium / high), y un indicador isTrendingInterest. Las publicaciones cuyas etiquetas coinciden con los intereses de alta afinidad de un usuario tienen mayor prioridad en su feed.
Componentes de frontend
| Componente | Descripción |
|---|---|
PostCard.tsx | Renderiza una sola publicación en el feed |
CreatePostModal.tsx | Modal para crear una publicación con carga de contenido multimedia |
SuggestedUsers.tsx | Panel lateral con sugerencias de cuentas a seguir |
page-components/HomePage.tsx | Página principal del feed |