How to Create Your Own LinkTree with Python and GitHub Pages
I stumbled upon an article by Lucas Neves Pereira titled "Build your own LinkTree with Go and GitHub Pages". The article describes how to create a LinkTree-like page (Taplink) using Go. As a Python enthusiast, I decided to implement the project in Python. Here’s what I came up with.
Step 1: Preparing the Project File Structure
First, let's create the file structure for our project. We’ll organize it to be easily maintainable and deployable on GitHub Pages.
File Structure:
/ (root)
|-- /docs
| |-- index.html
| |-- /assets
| |-- (styles, scripts, icons, etc.)
|-- config.yml
|-- generate_site.py
|-- /themes
-
/docs
: This folder will hold the generated HTML files and all necessary assets (images, styles, scripts). This folder will be used for deployment on GitHub Pages. -
config.yml
: Configuration file containing all data for personalizing the site. -
generate_site.py
: Python script that will generate the site based on data fromconfig.yml
. -
/themes
: Folder containing themes for the site. In our case, it has a singlecustom
theme that includes HTML templates, styles, scripts, and images.
Step 2: Configuring the Configuration File (config.yml
)
The config.yml
file contains user data and links that will be displayed on the site. Here’s its content:
name: "King Triton"
picture: "assets/img/picture.jpg"
bio: "Programmer python and php/laravel"
meta:
lang: "en"
description: "Programmer python and php/laravel"
title: "King Triton"
author: "King Triton"
siteUrl: "https://king-tri-ton.github.io"
links:
- name: "Github"
url: "https://github.com/king-tri-ton"
- name: "Dev.to"
url: "https://dev.to/king_triton"
- name: "Patreon"
url: "https://www.patreon.com/king_triton"
- name: "Telegram"
url: "https://t.me/king_triton"
- name: "Instagram"
url: "https://www.instagram.com/king_tri_ton"
theme: "custom"
-
name
: The user’s name that will be displayed on the site. -
picture
: Path to the user’s image. -
bio
: A brief biography of the user. -
meta
: Site metadata (language, description, title, author, site URL). -
links
: List of links to be displayed on the site. Each item includes a name and URL. -
theme
: The site theme to use.
Step 3: Developing the Python Script to Generate the Site (generate_site.py
)
Next, we’ll write a Python script that will use the theme template, data from config.yml
, and generate the HTML file.
import os
import shutil
from jinja2 import Environment, FileSystemLoader
import yaml
# Load configuration
with open('config.yml', 'r') as config_file:
config = yaml.safe_load(config_file)
# Create output directory
output_dir = 'docs'
os.makedirs(output_dir, exist_ok=True)
# Set up Jinja2
env = Environment(loader=FileSystemLoader('themes/custom'))
template = env.get_template('index.html')
# Generate HTML file
output_html = template.render(config=config)
with open(os.path.join(output_dir, 'index.html'), 'w') as fh:
fh.write(output_html)
# Copy assets folder to output directory
assets_source = os.path.join('themes', config['theme'], 'assets')
assets_dest = os.path.join(output_dir, 'assets')
if os.path.exists(assets_source):
shutil.copytree(assets_source, assets_dest, dirs_exist_ok=True)
print("Site generated successfully.")
-
Loading configuration: The script loads data from
config.yml
. -
Creating output directory: The
docs
folder is created automatically if it doesn’t exist. - Setting up Jinja2: Jinja2 is used to load the HTML template and render the content.
-
Generating HTML file: The script generates
index.html
using the configuration data and saves it in thedocs
folder. -
Copying assets: All assets (CSS, images, scripts) are copied to the
docs/assets
folder.
Step 4: Creating the Theme and Assets
Now, create the theme that will be used for the site. In the themes/custom/
folder, there should be the following files:
themes/custom/index.html
This is the main HTML template for the site, using variables from the configuration file.
<!DOCTYPE html>
<html lang="{{ config.meta.lang }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{ config.meta.description }}">
<title>{{ config.meta.title }}</title>
<meta name="author" content="{{ config.meta.author }}">
<link rel="canonical" href="{{ config.meta.siteUrl }}">
<link rel="icon" type="image/x-icon" href="assets/icons/favicon.ico">
<link rel="stylesheet" href="assets/css/styles.css">
<meta property="og:title" content="{{ config.meta.title }}">
<meta property="og:site_name" content="{{ config.meta.title }}">
<meta property="og:description" content="{{ config.meta.description }}">
<meta property="og:locale" content="{{ config.meta.lang }}">
<meta name="twitter:title" content="{{ config.meta.title }}">
<meta name="twitter:description" content="{{ config.meta.description }}">
</head>
<body>
<header>
<img src="{{ config.picture }}" alt="Picture" class="avatar">
<h1>{{ config.name }}</h1>
<small class="bio">{{ config.bio }}</small>
</header>
<main>
<section class="links">
{% for link in config.links %}
<a class="link-item" href="{{ link.url }}" target="_blank" rel="noopener noreferrer">
<p>{{ link.name }}</p>
</a>
{% endfor %}
</section>
</main>
<footer>
<small>© <span class="year"></span> {{ config.meta.author }}</small>
</footer>
<script src="assets/js/script.js"></script>
</body>
</html>
themes/custom/assets/styles.css
CSS file for styling the page.
/* CSS Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Variables */
:root {
--max-width: 600px;
--font-family: 'Inter', sans-serif;
--padding: 1rem;
--header-margin-bottom: 1rem;
--line-height: 2;
--font-size: 16px;
--primary-color-light: #ffffff;
--background-color-light: #f0f0f0;
--text-color-light: #333;
--link-color-light: #1a73e8;
--bio-color-light: #666;
--primary-color-dark: #1e1e1e;
--background-color-dark: #121212;
--text-color-dark: #e0e0e0;
--link-color-dark: #8ab4f8;
--bio-color-dark: #aaa;
}
/* Light Theme */
@media (prefers-color-scheme: light) {
:root {
--primary-color: var(--primary-color-light);
--background-color: var(--background-color-light);
--text-color: var(--text-color-light);
--link-color: var(--link-color-light);
--bio-color: var(--bio-color-light);
}
}
/* Dark Theme */
@media (prefers-color-scheme: dark) {
:root {
--primary-color: var(--primary-color-dark);
--background-color: var(--background-color-dark);
--text-color: var(--text-color-dark);
--link-color: var(--link-color-dark);
--bio-color: var(--bio-color-dark);
}
}
/* Global Styles */
html {
font-family: var(--font-family);
font-size: var(--font-size);
line-height: var(--line-height);
}
body {
max-width: var(--max-width);
min-height: 100vh;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--background-color);
color: var(--text-color);
padding: var(--padding);
}
/* Header Styles */
header {
padding: var(--padding) 0;
margin-bottom: var(--header-margin-bottom);
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--primary-color);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 24px;
margin-bottom: 0.5rem;
}
.bio {
font-size: 14px;
color: var(--bio-color);
margin-bottom: 1rem;
}
/* Main Content Styles */
main {
width: 100%;
flex: 1;
}
.links {
display: flex;
flex-direction: column;
gap: 1rem;
text-align: center;
overflow-y: auto;
max-height: 400px;
}
.link-item {
display: block;
padding: 16px 20px;
text-decoration: none;
color: var(--link-color);
background: var(--primary-color);
border-radius: 12px;
border: 1px solid var(--link-color);
transition: background-color 0.25s, color 0.25s;
}
.link-item:hover,
.link-item:focus {
background-color: var(--link-color);
color: var(--primary-color);
}
.link-item p {
line-height: 1.5;
font-weight: 500;
}
/* Footer Styles */
footer {
width: 100%;
text-align: center;
padding: 1rem 0;
font-size: 14px;
gap: 1rem;
display: flex;
justify-content: center;
align-items: center;
}
/* ScrollBar */
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: transparent;
}
::-webkit-scrollbar-thumb:hover {
background: transparent;
}
themes/custom/assets/js/script.js
JavaScript file for basic functionalities.
console.log("scripts loaded");
const yearDate = new Date().getFullYear().toString();
document.querySelector(".year").innerText = yearDate;
themes/custom/assets/img/picture.jpg
The photo used as the avatar.
Step 5: Generating the Site
After creating all the files, run the generate_site.py
script to generate the site:
python generate_site.py
The site will be generated in the docs
folder.
Step 6: Deploying on GitHub Pages
- Create a new repository on GitHub.
- Upload all files, including the
docs
folder, to the repository. - Go to the repository’s Settings section.
- In the Pages section, select the
master
branch and the/docs
folder as the source. - Save changes and wait for GitHub Pages to deploy your site.
Your site will now be available at https://<username>.github.io/<repository-name>/
.
And that’s it! You now have your own LinkTree-style site created with Python and deployed on GitHub Pages. You can check the final result at https://king-tri-ton.github.io/pythonpagelink/.
Top comments (2)
Neat
thanks