Los acortadores de enlaces sirven para simplificar la manera en que compartimos y gestionamos los enlaces en línea. En el siguiente artículo, verás la estructura básica de la base de datos para poder desarrollar un proyecto de este tipo y el proyecto implementado en NextJS con ayuda de Supabase para la autenticación y la base de datos. Cabe aclarar que Supabase simplifica el desarrollo de aplicaciones al proporcionar una infraestructura de backend completa y fácil de usar.
Para este proyecto se utilizará una estructura básica de base de datos para simplicar el desarrollo de la aplicación. Tendremos solo 3 tablas: users, links y visits_links.
Cabe resaltar que links tendrá de llave foránea el id de users y visits_links tendrá de llave foránea el id de la tabla link.
De manera general verás los puntos claves de la aplicación para que tenga el funcionamiento que queremos. Tu puedes organizar las rutas de la aplicación como quieras. Todo estará de manera resumida en este artículo, para ver el código completo de la aplicación haz click aquí.
Nos saltaremos toda la conexión con Supabase y como hacer autenticación con github, estos pasos los puedes conseguir en la documentación de Supabase.
export function createClient () {
const cookieStore = cookies()
// El tipo Database se puede obtener gracias a un comando de Supabase.
// Para más información consulta la documentación de Supabase
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get (name: string) {
return cookieStore.get(name)?.value
}
}
}
)
}
'use client'
export function createClient () {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
Para guardar los links utilizamos los server actions de NextJS.
export const FormCreateLink = () => {
const addLink = async(formData: FormData) => {
'use server'
// obtenemos el name y url del formulario
const name = formData.get('name')?.toString().replace(' ', '-')
const url = formData.get('url')?.toString()
// Verificamos si la url comienza con el protocolo http
if (!url.startsWith('http')) {
throw new Error('URL invalida')
}
// Verificamos si el name no exite
const supabase = createClient()
const { data, error: errorSelect } = await supabase.from('links').select('*').eq('name', name)
if (errorSelect) {
throw new Error('Error al guardar el enlace')
}
if (data.length !== 0) {
throw new Error('El nombre ya existe')
}
// Guardamos el link
const { data: { user } } = await supabase.auth.getUser()
const shortUrl = `https://dominio.ejemplo/${name}`
const { error } = await supabase.from('links').insert({ name, url, short_url: shortUrl, uid_user: user?.id })
if (error) {
throw new Error('Error al guardar el enlace')
}
}
return (
<form action={addLink}>
<input type='text' name='name' placeholder='Nombre del enlace' required />
<input type='text' name='url' placeholder='URL' required />
<button>Duardae enlace</button>
</form>
)
}
Recuerda que para ver, guardar, actualizar y actualizar debes editar las políticas de la base de datos de Supabase.
interface Props {
link: TypeLink
}
export const FormEditLink: FC<Props> = ({ link }) => {
const editLink = async(formData: FormData) => {
'use server'
// obtenemos el name del formulario
const name = formData.get('name')?.toString().replace(' ', '-')
// Verificamos si el name no exite
const supabase = createClient()
const { data, error: errorSelect } = await supabase.from('links').select('*').eq('name', name)
if (errorSelect) {
throw new Error('Error al guardar el enlace')
}
if (data.length !== 0) {
throw new Error('El nombre ya existe')
}
// Guardamos el link editado
const shortUrl = `https://dominio.ejemplo/${name}`
const { error } = await supabase
.from('links')
.update({ name, short_url: shortUrl })
.eq('id', id)
if (error) {
throw new Error('Error al guardar el enlace')
}
}
return (
<form action={editLink}>
<input type='text' name='name' placeholder='Nombre del enlace' />
<button>Editar enlace</button>
</form>
)
}
Para borrar un link podríamos utilizar un botón con la siguiente funcionalidad.
export const DeleteButton = () => {
const handleDelete = async () => {
const { error } = await supabase.from('links').delete().eq('id', link.id)
if (error) {
throw new Error('Error al eliminar el link')
}
}
return (
<button type='button' onClick={handleDelete}>
// icono de la caneca de basura
<TrashIcon />
</button>
)
}
Este es el punto clave de la aplicación. Vamos a crear la ruta /[name]
, esta nos va a servir para obtener el nombre del link, buscar el enlace por dicho nombre en la base de datos y redirigir a la URL original.
Por ejemplo, un usuario x
da click en el enlace https://dominio.ejemplo/rubertweb
, la aplicacion extrae el nombre de la URL rubertweb
, obtiene el link que coincida con el nombre dado de la base de datos y redirige a la URL original que en este caso sería https://rubertweb.dev
.
Recordemos que el link que se obtiene de la tabla links tienen las propiedades nombre, url personalizada o corta, url original, id del usuario que la creó y la fecha de creación.
El código del archivo /[name]/page.tsx
quedaria de la siguiente manera:
interface Props {
params: {
name: string
}
}
const RedirectingPage: FC<Props> = async ({ params: { name } }) => {
const supabase = createClient()
const { data: links, error } = await supabase.from('links').select('*').eq('name', decodeURIComponent(name))
if (error != null || links == null || links?.length === 0) {
return redirect('/')
}
return redirect(links[0].url)
}
export default RedirectingPage
Para guadar el click hecho en un enlace vamos a agregar una funcionalidad al componente anterior.
interface Props {
params: {
name: string
}
}
const RedirectingPage: FC<Props> = async ({ params: { name } }) => {
const supabase = createClient()
const { data: links, error } = await supabase.from('links').select('*').eq('name', decodeURIComponent(name))
if (error != null || links == null || links?.length === 0) {
return redirect('/')
}
// La siguiente linea inserta en la tabla visits_links un nuevo registro con la id del link.
// La base de datos agrega id y fecha de creacion automáticamente
await supabase.from('visits_links').insert({ uid_link: links[0].id })
return redirect(links[0].url)
}
export default RedirectingPage
Con estos componentes tendrás las funcionalidades básicas de la aplicación. Recuerda que puedes ver el código completo del ejemplo en mi repositorio de Github. Tambien me puedes contactar para mas información en mi página de contacto ❤️.