For noget tid siden fortalte jeg om en prototype på en linkforkorter, jeg havde lavet. Den var ikke særlig brugbar, for den lå på wallnot.dk og lavede derfor ikke specielt korte links.
Efter en behagelig og ukompliceret dialog med de flinke advokater hos Kønig Advokater, der ejede brugsretten til domænenavnet lnk.dk, har jeg fået lov til at overtage lnk.dk – og nu er en opdateret udgave af min kortlinkservice i luften.
På lnk.dk kan du lave automatisk generede korte links (tænk lnk.dk/ab0g) eller selv vælge, hvad dit korte link skal hedde (tænk lnk.dk/morten).
Som sædvanlig har jeg brugt Django til arbejdet.
I models.py definerer jeg datamodellen, lidt ekstra validering til brug i formularen til at lave korte links, og hvad jeg gerne vil se i admin-interfacet for siden:
from django.db import models from django.utils import timezone from django.contrib import admin from django.core.exceptions import ValidationError def validate_destination(destination): if "lnk.dk/" in destination.lower(): raise ValidationError('For at undgå risiko for uendelige viderestillinger, kan du ikke tilføje korte links fra lnk.dk som destination.') def validate_shortlink(shortlink): if shortlink == "om": raise ValidationError('Det korte link "om" bruger lnk.dk til at fortælle om lnk.dk. Vælg et andet selvvalgt kort link.') elif shortlink == "administration": raise ValidationError('Det korte link "administration" bruger lnk.dk til at administrere lnk.dk. Vælg et andet selvvalgt kort link.') class Link(models.Model): destination = models.URLField('Destinationslink', max_length=65535, validators=[validate_destination]) shortlink = models.SlugField('Kort link', max_length=100, unique=True, allow_unicode=True, validators=[validate_shortlink]) LINK_TYPE_CHOICES = ( ('automatic', 'Automatisk'), ('manual', 'Manuelt'), ) type = models.CharField('Type', max_length=10, choices=LINK_TYPE_CHOICES) date = models.DateTimeField(default=timezone.now, editable=False) class LinkAdmin(admin.ModelAdmin): list_display = ('destination','shortlink','type','date') list_filter = ('type', ) search_fields = ['destination']
I forms.py definerer jeg formularen, som brugeren indtaster sit lange link og evt. et selvvalgt kort link i. Jeg forsøger også at formulere nogle forståelige fejlmeddelelser:
from django.forms import ModelForm from .models import Link class LinkForm(ModelForm): def __init__(self, *args, **kwargs): super(LinkForm, self).__init__(*args, **kwargs) self.fields['destination'].widget.attrs['placeholder'] = 'https://eksempel.dk/meget/lang/url' self.fields['shortlink'].widget.attrs['placeholder'] = 'eksempel' self.fields['shortlink'].label_suffix = "" # Remove colon after label self.fields['shortlink'].required = False # Not required in form class Meta: model = Link fields = ['destination', 'shortlink'] labels = { 'shortlink': ('Evt. selvvalgt kort link, lnk.dk/'), } error_messages = { 'destination': { 'max_length': ('Din destinationsurl er for lang til denne kortlinkservice.'), 'invalid': ('Din destinationsurl er ikke en gyldig adresse. Husk http://, https:// eller ftp:// foran dit link, hvis du har glemt det.'), }, 'shortlink': { 'unique': ('Det selvvalgte link, du har valgt, er allerede i brug. Find på et andet.'), 'max_length': ('Dit selvvalgte link må maksimalt være 100 tegn langt.'), 'invalid': ('Du kan kun bruge unicode-bogstaver, cifre, bindestreg og understreg i din selvvalgte adresse.'), } }
Mine visninger forberedes i views.py som har:
- En funktion til autogenerede kortlinks. Linket tilføjet en tilfældig streng hashes til en ny tilfældig streng. Der tilføjes en tilfældig streng hver gang for at sikre, at der genereres en ny streng hver gang (for at undgå en uendelig løkke, hvis et links hash-værdi skulle kollidere med et andet links hashværdi).
- En visning til forsiden med dens formular, validering af formularen og visning af kortlink og eventuelle fejl.
- En visning, der sørger for at viderestille fra et kort link til et destinationslink, hvis det korte link findes. Ellers vises en fejlside.
- En visning til en “om lnk.dk”-side.
from django.shortcuts import render from django.http import HttpResponseRedirect from .models import Link from .forms import LinkForm from django.urls import reverse import hashlib import bcrypt # Function to create a random hash to use as short link address def create_shortlink(destination): salt = bcrypt.gensalt().decode() # Random salt destination = destination+salt # Salt added to destination URL hash = hashlib.md5(destination.encode()).hexdigest() # Hashed to alphanumeric string return hash[:4] # First 5 characters of that string # Front page with a form to enter destination address. Short URL returned. def linkindex(request): form = LinkForm() # Loads form # If a destination is submitted, a short link is returned if request.method == 'POST': form = LinkForm(request.POST) # Form instance with submitted data # Check whether submitted data is valid if form.is_valid(): destination = form.cleaned_data['destination'] # Submitted destination shortlink = form.cleaned_data['shortlink'] # Submitted slug # User has specified a unique (validated) short link if shortlink: link = form.save(commit=False) link.type = "manual" link = form.save() site_url = reverse('redirect', args=[link.shortlink]) sharelink = request.build_absolute_uri(site_url) # User wants an automatic link else: # If a short link with same destionation of same type already exists, # it is fetched from database and served. No need to use up a new # URL for the same destination. try: link = Link.objects.get(destination=destination, type="automatic") site_url = reverse('redirect', args=[link.shortlink]) sharelink = request.build_absolute_uri(site_url) # If a link of same type with same destination does not exist, one is # created. except: # Loop to create a unique hash value for short link unique_link = False while unique_link == False: hash = create_shortlink(destination) # Return hash # First we check whether the hash is a duplicate try: Link.objects.get(shortlink=hash) # Check whether hash is used # If not a duplicate, an error is thrown, and we can save the hash except: link = form.save(commit=False) # Prepare to save form destination data and hash link.shortlink = hash # Sets short link to hash value link.type = "automatic" link.save() # Saves destination and short link to database site_url = reverse('redirect', args=[link.shortlink]) sharelink = request.build_absolute_uri(site_url) unique_link = True # If check causes error, hash is unused, exit loop context = {'sharelink': sharelink, 'form': form} # Dictionary with variables used in template return render(request, 'links/index.html', context) # If form is invalid, check whether a user is trying to create a duplicate manual # shortlink. else: # Check if there's a valid destination link if 'destination' in form.cleaned_data: # Check if there's a shortlink that's not unique try: link = Link.objects.get(shortlink=form.data['shortlink']) # If so, check whether the destination is the same if form.cleaned_data['destination'] == link.destination: # Show sharelink to user site_url = reverse('redirect', args=[link.shortlink]) sharelink = request.build_absolute_uri(site_url) form.errors['shortlink'] = "" # Error replaced by empty string # Render form with sharelink already used error else: sharelink = "" # Render form with error except: sharelink = "" # Render form with errors else: sharelink = "" context = {'form': form, 'sharelink': sharelink} return render(request, 'links/index.html', context) # Render page with form before user has submitted context = {'form': form} return render(request, 'links/index.html', context) # Short link redirect to destination URL def redirect(request, shortlink): # Query the database for short link, if there is a hit, redirect to destination URL try: link = Link.objects.get(shortlink=shortlink) return HttpResponseRedirect(link.destination) # An error means the short link doesn't exist, so error 404 is shown except: return render(request, 'links/404.html', status=404) # About page def about(request): context = {'request': request} return render(request, 'links/about.html', context)
urls.py sørger for at forbinde den adressse, brugeren har tastet i browseren, med de rette visninger fra views.py:
from django.urls import path from . import views urlpatterns = [ path('', views.linkindex, name='index'), path('om', views.about, name='about'), path('<shortlink>', views.redirect, name='redirect'), ]
Endelig har jeg skabelon-filer, der sørger for selve html-koden på siden. base.html er min overordnede skabelon med det overordnede design, meta-tags, sidefod osv.:
{% load static %}{% spaceless %}<!doctype html> <html lang="da"> <head> <title>lnk.dk: Danmarks korteste links</title> <meta charset="utf-8"/> <meta name="description" content="Lav de korteste korte kortlinks gratis på lnk.dk. Fri for annoncer og overvågning."> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <link rel="stylesheet" href="{% static "links/style.css" %}"> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> <link rel="manifest" href="/site.webmanifest"> <meta name="theme-color" content="#ffffff"> </head> <body> {% block content %}{% endblock %} <div class="footer"> <p>Lav de korteste korte links gratis på lnk.dk. Fri for annoncer og overvågning. <a href="{% url 'about' %}">Om lnk.dk</a></p> </div> </body> </html> {% endspaceless %}
Og her er skabelonen til forsiden, index.html:
{% extends "links/base.html" %}{% block content %}{% spaceless %} <h1>Lav et kort link</h1> <div class="content"> {% if form %} <form method="post"> {% csrf_token %} <div class="form_field"> <div class="label"> {{ form.destination.label_tag}} </div> <div>{{ form.destination }}</div> {{ form.destination.errors }} </div> <div class="form_field"> <div class="label"> {{ form.shortlink.label_tag}} </div> <div>{{ form.shortlink }}</div> {{ form.shortlink.errors }} </div> <p><button type="submit" value="Giv mig et kort link">Giv mig et kort link</button></p> </form> {% if request.method == "POST" and not form.destination.errors and not form.shortlink.errors %} <h1>Her er dit link:</h1> <p class="sharelink"><a href="{{ sharelink }}">{{ sharelink }}</a></p> <button class="copy">Kopier link</button> {% endif %} {% endif %} {% if error %} <h2>Har du tastet forkert?</h2> <p><em>Du har prøvet at bruge et kort link. Desværre er det link, du har tastet, ikke registreret. Måske er du kommet til at taste forkert?</em></p> <p><a href="{% url 'index' %}">Til forsiden</a> {% endif %} </div> {% if request.method == "POST" and not form.destination.errors %} <script> function fallbackCopyTextToClipboard(text) { var textArea = document.createElement("textarea"); textArea.value = text; document.body.appendChild(textArea); textArea.focus(); textArea.select(); document.execCommand("copy"); document.body.removeChild(textArea); } function copyTextToClipboard(text) { if (!navigator.clipboard) { fallbackCopyTextToClipboard(text); return; } navigator.clipboard.writeText(text); } var copy = document.querySelector('.copy'); copy.addEventListener('click', function(event) { copyTextToClipboard('{{ sharelink }}'); }); </script> {% endif %} {% endspaceless %}{% endblock %}
Er du nået hertil? Se det i praksis på lnk.dk!