{
+ const { slug } = await params;
+ const post = await fetchPostData(slug);
+
+ if (!post) {
+ return {
+ title: 'No encontrado - Almeira Web',
+ description: 'La publicación solicitada no existe o ha sido eliminada.'
+ };
+ }
+
+ const title = decodeHtmlEntities(post.titulo);
+ const description = post.resumen_redes
+ ? decodeHtmlEntities(post.resumen_redes)
+ : (post.contenido?.replace(/<[^>]*>?/gm, '').substring(0, 160) + '...');
+
+ const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
+ const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
+ let imagenUrl = post.og_image_url || null;
+
+ if (post.imagen_destacada_id) {
+ try {
+ const client = await createAdminClient();
+ const mediaRes = await client.databases.getDocument(APPWRITE_ALMEIRA_CONFIG.databaseId, 'media', post.imagen_destacada_id);
+ if (mediaRes) {
+ imagenUrl = `${ENDPOINT}/storage/buckets/${mediaRes.bucket_id}/files/${mediaRes.file_id}/preview?project=${PROJECT_ID}&width=1200&height=630&quality=90`;
+ }
+ } catch(e) {}
+ }
+
+ const previousImages = (await parent).openGraph?.images || [];
+ const images = imagenUrl ? [imagenUrl, ...previousImages] : previousImages;
+
+ return {
+ title: title,
+ description: description,
+ openGraph: {
+ title: title,
+ description: description,
+ images: images,
+ type: 'article',
+ publishedTime: post.fecha_publicacion || post.$createdAt,
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: title,
+ description: description,
+ images: images,
+ }
+ };
+}
+
export default async function NoticiaDetail({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await fetchPostData(slug);
@@ -110,9 +179,9 @@ export default async function NoticiaDetail({ params }: { params: Promise<{ slug
({ ...doc })) };
+ } catch (error: any) {
+ console.error('Error fetching media:', error);
+ return { success: false, data: [] };
+ }
+}
+
+export async function getMediaServer(documentId: string) {
+ try {
+ const client = getAdminClient();
+ const databases = new Databases(client);
+ const doc = await databases.getDocument(DATABASE_ID, 'media', documentId);
+ return { success: true, data: { ...doc } };
+ } catch (error: any) {
+ console.error('Error fetching media document:', error);
+ return { success: false, data: null };
+ }
+}
diff --git a/src/app/robots.ts b/src/app/robots.ts
new file mode 100644
index 0000000..8d1c929
--- /dev/null
+++ b/src/app/robots.ts
@@ -0,0 +1,14 @@
+import { MetadataRoute } from 'next';
+
+export default function robots(): MetadataRoute.Robots {
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://almeira.com.co';
+
+ return {
+ rules: {
+ userAgent: '*',
+ allow: '/',
+ disallow: ['/dashboard/', '/api/'],
+ },
+ sitemap: `${baseUrl}/sitemap.xml`,
+ };
+}
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
new file mode 100644
index 0000000..9723374
--- /dev/null
+++ b/src/app/sitemap.ts
@@ -0,0 +1,41 @@
+import { MetadataRoute } from 'next';
+import { createAdminClient } from '@/lib/appwrite.server';
+import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite';
+import { Query } from 'node-appwrite';
+
+export default async function sitemap(): Promise {
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://almeira.com.co';
+
+ // Static routes
+ const routes = ['', '/clasificados', '/eventos', '/tv', '/radio'].map((route) => ({
+ url: `${baseUrl}${route}`,
+ lastModified: new Date(),
+ changeFrequency: 'daily' as const,
+ priority: 1,
+ }));
+
+ // Dynamic posts
+ let postRoutes: MetadataRoute.Sitemap = [];
+ try {
+ const client = await createAdminClient();
+ const dbId = APPWRITE_ALMEIRA_CONFIG.databaseId;
+
+ // Fetch all published posts
+ const postsRes = await client.databases.listDocuments(dbId, 'posts', [
+ Query.equal('estado', 'Publicado'),
+ Query.limit(1000), // Get up to 1000 posts. For more, pagination is needed.
+ Query.orderDesc('fecha_publicacion')
+ ]);
+
+ postRoutes = postsRes.documents.map((post) => ({
+ url: `${baseUrl}/${post.slug || post.$id}`,
+ lastModified: new Date(post.$updatedAt || post.fecha_publicacion || post.$createdAt),
+ changeFrequency: 'weekly' as const,
+ priority: 0.7,
+ }));
+ } catch (e) {
+ console.error('Error generating sitemap for posts:', e);
+ }
+
+ return [...routes, ...postRoutes];
+}
diff --git a/src/components/dashboard/EntityForm.tsx b/src/components/dashboard/EntityForm.tsx
index 6e9d26c..1c6e299 100644
--- a/src/components/dashboard/EntityForm.tsx
+++ b/src/components/dashboard/EntityForm.tsx
@@ -118,7 +118,7 @@ export const EntityForm = ({
// Ensure featured image ID is set if present
if (featuredImageId) {
- formData.set('imagen_destacada_id', featuredImageId);
+ formData.set('logotipo', featuredImageId);
}
startTransition(async () => {
@@ -127,7 +127,7 @@ export const EntityForm = ({
};
// Organize fields into Main Content and Sidebar Metadata
- const sidebarFieldNames = ['estado', 'fecha_publicacion', 'fecha_finalizacion', 'slug', 'imagen_destacada_id', 'categoria_id', 'tipo_publicacion_id'];
+ const sidebarFieldNames = ['estado', 'fecha_publicacion', 'fecha_finalizacion', 'slug', 'logotipo', 'categoria_id', 'tipo_publicacion_id'];
const mainFields = fields.filter(f => !sidebarFieldNames.includes(f.name));
const sidebarFields = fields.filter(f => sidebarFieldNames.includes(f.name));
@@ -136,8 +136,9 @@ export const EntityForm = ({
// Set smart defaults for new items
if (!initialData) {
- if (field.name === 'estado') return 'borrador';
- if (field.name === 'fecha_publicacion' || field.type === 'date') {
+ if (field.name === 'logotipo') {
+ return featuredImageId;
+ } if (field.name === 'fecha_publicacion' || field.type === 'date') {
// Get local ISO string for datetime-local input
const now = new Date();
return new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
diff --git a/src/components/dashboard/MediaGridSelector.tsx b/src/components/dashboard/MediaGridSelector.tsx
index 6a2a556..7f1d3b9 100644
--- a/src/components/dashboard/MediaGridSelector.tsx
+++ b/src/components/dashboard/MediaGridSelector.tsx
@@ -4,8 +4,7 @@ import { useState, useEffect, useCallback, useRef } from 'react';
import { createPortal } from 'react-dom';
import { ImageIcon, X, Search, CheckCircle2, UploadCloud, Loader2 } from 'lucide-react';
import { databases, APPWRITE_ALMEIRA_CONFIG, endpoint, projectId } from '@/lib/appwrite';
-import { Query } from 'appwrite';
-import { uploadMediaDocumentServer } from '@/app/actions/appwrite';
+import { uploadMediaDocumentServer, listMediaServer, getMediaServer } from '@/app/actions/appwrite';
/** Utility to generate frontend Appwrite image previews */
export function getAppwriteImageUrl(fileId: string, bucketId?: string, width = 400, height = 250) {
@@ -52,25 +51,18 @@ export const MediaGridSelector = ({
setIsLoading(true);
try {
const currentOffset = more ? mediaItems.length : 0;
- const queries = [
- Query.limit(20),
- Query.offset(currentOffset),
- Query.orderDesc('$createdAt')
- ];
-
- if (search) {
- queries.push(Query.contains('titulo', search));
- }
-
- const res = await databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'media', queries);
+ const res = await listMediaServer(search, currentOffset);
- if (more) {
- setMediaItems(prev => [...prev, ...res.documents]);
+ if (res.success && res.data) {
+ if (more) {
+ setMediaItems(prev => [...prev, ...res.data]);
+ } else {
+ setMediaItems(res.data);
+ }
+ setHasMore(res.data.length === 20);
} else {
- setMediaItems(res.documents);
+ setHasMore(false);
}
-
- setHasMore(res.documents.length === 20);
} catch (error) {
console.error('Error fetching media:', error);
} finally {
@@ -92,13 +84,14 @@ export const MediaGridSelector = ({
useEffect(() => {
// If we have a value but haven't loaded its document, fetch just that one
if (value && mode === 'field' && !mediaItems.some(m => m.$id === value)) {
- databases.getDocument(APPWRITE_ALMEIRA_CONFIG.databaseId, 'media', value)
- .then(doc => {
- setMediaItems(prev => {
- // Protect against race conditions duplicating the doc
- if (prev.some(m => m.$id === value)) return prev;
- return [...prev, doc];
- });
+ getMediaServer(value)
+ .then(res => {
+ if (res.success && res.data) {
+ setMediaItems(prev => {
+ if (prev.some(m => m.$id === value)) return prev;
+ return [...prev, res.data];
+ });
+ }
}).catch(err => console.error("Error resolver media thumbnail:", err));
}
}, [value, mode]);
@@ -306,7 +299,10 @@ export const MediaGridSelector = ({
>
)}
-
+ {/* Sólo incluir hidden input en modo campo, para evitar conflictos cuando se usa como modal puro */}
+ {mode === 'field' && label !== 'Logotipo Corporativo' && (
+
+ )}
{renderModal()}
);
diff --git a/test_html.js b/test_html.js
new file mode 100644
index 0000000..3eb9e56
--- /dev/null
+++ b/test_html.js
@@ -0,0 +1,3 @@
+const DOMPurify = require('isomorphic-dompurify');
+const txt = "Line 1\nLine 2\n\nLine 3\nBold text";
+console.log(DOMPurify.sanitize(txt));