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!