Best Practices
This guide provides best practices for creating PreMiD Activities. Following these practices will help you create high-quality activities that are easy to maintain and provide a good user experience.
Code Quality
Use TypeScript Properly
TypeScript is required for all PreMiD Activities. Make sure you're using TypeScript's features properly to get the most benefit from it:
// Good - Using proper type annotations
const presence = new Presence({
clientId: 'your_client_id'
})
const browsingTimestamp = Math.floor(Date.now() / 1000)
enum ActivityAssets {
Logo = 'https://example.com/logo.png',
}
presence.on('UpdateData', async () => {
const presenceData: PresenceData = {
largeImageKey: ActivityAssets.Logo,
details: 'Browsing Example.com',
state: 'Homepage',
startTimestamp: browsingTimestamp
}
presence.setActivity(presenceData)
})
// Bad - Missing type annotations, not using enum for assets
const presence = new Presence({
clientId: 'your_client_id'
})
const browsingTimestamp = Math.floor(Date.now() / 1000)
presence.on('UpdateData', async () => {
// Missing PresenceData type annotation
const presenceData = {
largeImageKey: 'https://example.com/logo.png',
details: 'Browsing Example.com',
state: 'Homepage',
startTimestamp: browsingTimestamp
}
presence.setActivity(presenceData)
})
Handle Errors
Always check if elements exist before trying to access their properties. This will prevent your activity from crashing if the page structure changes.
// Good
const videoTitle = document.querySelector('.video-title')?.textContent || 'Unknown video'
// Bad
const videoTitle = document.querySelector('.video-title').textContent
Use Destructuring
Use destructuring to access object properties for cleaner and more readable code.
// Good
const { pathname, hostname, href } = document.location
// Bad
const pathname = document.location.pathname
const hostname = document.location.hostname
const href = document.location.href
Use Constants
Define constants for repeated values to make your code more maintainable. Always define constants outside of the UpdateData event for better performance. Use the enum
keyword to define constants for assets.
// Good
const SLIDESHOW_TIMEOUT = 5000 // 5 seconds
enum ActivityAssets {
Logo = 'https://example.com/logo.png',
}
slideshow.addSlide('slide1', {
details: 'Browsing Example.com',
state: 'Homepage',
largeImageKey: ActivityAssets.Logo
}, SLIDESHOW_TIMEOUT)
// Bad
slideshow.addSlide('slide1', {
details: 'Browsing Example.com',
state: 'Homepage',
largeImageKey: 'https://example.com/logo.png'
}, 5000)
Comment Your Code
Add meaningful comments to explain complex logic, non-obvious decisions, or important context. Avoid commenting on self-explanatory code.
// Good
// Process video content when available and ready
const video = document.querySelector('video')
if (video && video.readyState > 0) {
const title = document.querySelector('.video-title')?.textContent || 'Unknown video'
const author = document.querySelector('.video-author')?.textContent || 'Unknown author'
const isPlaying = !video.paused
// Set activity data for video content
presenceData.type = ActivityType.Watching
presenceData.details = title
presenceData.state = `By ${author}`
}
// Bad - No comments for complex logic
const video = document.querySelector('video')
if (video && video.readyState > 0) {
const title = document.querySelector('.video-title')?.textContent || 'Unknown video'
const author = document.querySelector('.video-author')?.textContent || 'Unknown author'
const isPlaying = !video.paused
presenceData.type = ActivityType.Watching
presenceData.details = title
presenceData.state = `By ${author}`
}
// Also Bad - Excessive comments for obvious code
// Create a variable to store the video element
const video = document.querySelector('video')
// Check if the video exists and is ready
if (video && video.readyState > 0) {
// Get the title of the video
const title = document.querySelector('.video-title')?.textContent || 'Unknown video'
// Get the author of the video
const author = document.querySelector('.video-author')?.textContent || 'Unknown author'
// Check if the video is playing
const isPlaying = !video.paused
// Set the activity type to watching
presenceData.type = ActivityType.Watching
// Set the details to the title
presenceData.details = title
// Set the state to the author
presenceData.state = `By ${author}`
}
Follow the Style Guide
Use consistent formatting and naming conventions. This makes your code easier to read and maintain.
// Good
const presence = new Presence({
clientId: 'your_client_id'
})
const SLIDESHOW_TIMEOUT = 5000
function getVideoInfo() {
const video = document.querySelector('video')
return {
title: document.querySelector('.video-title')?.textContent || 'Unknown video',
author: document.querySelector('.video-author')?.textContent || 'Unknown author',
isPlaying: video ? !video.paused : false
}
}
// Bad
const presence = new Presence({ clientId: 'your_client_id' })
const slideshow_timeout = 5000
function get_video_info() {
const video = document.querySelector('video')
return { title: document.querySelector('.video-title')?.textContent || 'Unknown video', author: document.querySelector('.video-author')?.textContent || 'Unknown author', isPlaying: video ? !video.paused : false }
}
Performance
Minimize DOM Queries
Cache DOM elements that you use multiple times to avoid unnecessary DOM queries.
// Good
const video = document.querySelector('video')
const title = document.querySelector('.video-title')?.textContent || 'Unknown video'
const author = document.querySelector('.video-author')?.textContent || 'Unknown author'
if (video && video.readyState > 0) {
presenceData.details = title
presenceData.state = `By ${author}`
if (!video.paused) {
// Use video
}
}
// Bad
if (document.querySelector('video') && document.querySelector('video').readyState > 0) {
presenceData.details = document.querySelector('.video-title')?.textContent || 'Unknown video'
presenceData.state = `By ${document.querySelector('.video-author')?.textContent || 'Unknown author'}`
if (!document.querySelector('video').paused) {
// Use video
}
}
Use Efficient Selectors
Use efficient selectors to find elements on the page. IDs are the fastest, followed by classes, then tag names. However, for elements that are typically unique on a page (like video or audio elements), using tag selectors can be appropriate.
// Good
const videoWithId = document.getElementById('video') // Best if ID exists
const title = document.querySelector('.video-title') // Using class selector
const video = document.querySelector('video') // Good for pages with a single video
// Less preferred
const title = document.getElementsByClassName('video-title')[0] // Works but querySelector is more consistent
const videos = document.querySelectorAll('video') // Unnecessary if you only need the first video
const firstVideo = videos[0]
Avoid Heavy Computations
Avoid heavy computations in the UpdateData
event, as it is fired regularly. If you need to perform heavy computations, cache the results.
// Good
let cachedData = null
let lastComputation = 0
presence.on('UpdateData', async () => {
const now = Date.now()
// Only compute data every 5 seconds
if (!cachedData || now - lastComputation > 5000) {
cachedData = computeHeavyData()
lastComputation = now
}
// Use cachedData
})
// Bad
presence.on('UpdateData', async () => {
const data = computeHeavyData()
// Use data
})
User Experience
Provide Clear Information
Make sure the information displayed in your activity is clear and easy to understand.
// Good
presenceData.type = ActivityType.Watching
presenceData.details = 'The Title of the Video' // No need for 'Watching:' prefix when type is set
presenceData.state = 'By: The Author of the Video'
// Bad
presenceData.details = 'Video'
presenceData.state = 'Author'
// Also Bad
presenceData.details = 'Watching: The Title of the Video' // Redundant with ActivityType.Watching
Use Appropriate Activity Types
Use the appropriate activity type for your activity. For example, use ActivityType.Watching
for video content and ActivityType.Listening
for audio content.
// Good
if (document.querySelector('video')) {
presenceData.type = ActivityType.Watching
}
else if (document.querySelector('audio')) {
presenceData.type = ActivityType.Listening
}
// Bad
presenceData.type = ActivityType.Playing
Add Timestamps
Add timestamps to show how long the user has been doing an activity or how much time is left.
// Good
// Create browsing timestamp outside UpdateData
let browsingTimestamp = Math.floor(Date.now() / 1000)
let wasWatchingVideo = false
presence.on('UpdateData', async () => {
// ...
if (isWatchingVideo) {
// Show remaining time for media
const video = document.querySelector('video')
if (video && video.readyState > 0) {
// Use destructuring assignment
[presenceData.startTimestamp, presenceData.endTimestamp]
= getTimestamps(video.currentTime, video.duration)
}
wasWatchingVideo = true
}
else {
// Only update browsing timestamp when changing state
if (wasWatchingVideo) {
browsingTimestamp = Math.floor(Date.now() / 1000)
wasWatchingVideo = false
}
presenceData.startTimestamp = browsingTimestamp
}
})
// Bad
// Updating browsing timestamp on every UpdateData event
presence.on('UpdateData', async () => {
presenceData.startTimestamp = Math.floor(Date.now() / 1000) // Don't do this!
})
// Bad
// Not using destructuring with getTimestamps
const timestamps = getTimestamps(video.currentTime, video.duration)
presenceData.startTimestamp = timestamps[0]
presenceData.endTimestamp = timestamps[1]
Use Settings
Add settings to allow users to customize your activity. This gives users more control over what information is displayed.
// Good
const showButtons = await presence.getSetting<boolean>('showButtons')
const showTimestamp = await presence.getSetting<boolean>('showTimestamp')
if (showButtons) {
presenceData.buttons = [
{
label: 'Visit Website',
url: document.location.href
}
]
}
if (showTimestamp) {
presenceData.startTimestamp = Date.now()
}
// Bad
// No settings, hardcoded behavior
presenceData.buttons = [
{
label: 'Visit Website',
url: document.location.href
}
]
presenceData.startTimestamp = Date.now()
Add Multilanguage Support
Add multilanguage support to your activity to provide a better experience for users who don't speak English.
// Good
const strings = await presence.getStrings({
play: 'general.playing',
pause: 'general.paused',
browse: 'general.browsing'
})
if (video) {
if (video.paused) {
presenceData.details = strings.pause
}
else {
presenceData.details = strings.play
}
}
else {
presenceData.details = strings.browse
}
// Bad
if (video) {
if (video.paused) {
presenceData.details = 'Paused'
}
else {
presenceData.details = 'Playing'
}
}
else {
presenceData.details = 'Browsing'
}
Maintenance
Keep Your Activity Updated
Websites change over time, so you need to keep your activity updated to ensure it continues to work correctly.
- Monitor website changes: Regularly check if the website has changed and update your activity accordingly.
- Update dependencies: Keep your dependencies up to date.
- Respond to issues: Respond to issues reported by users and fix them promptly.
Write Tests
Write tests for your activity to ensure it works correctly. This will help you catch errors before they affect users.
- Test with different page states: Test your activity with different page states to ensure it works correctly in all scenarios.
- Test with different settings: Test your activity with different settings to ensure they work correctly.
- Test with different browsers: Test your activity with different browsers to ensure it works correctly in all browsers.
Document Your Code
Document your code to make it easier for others to understand and contribute to your activity.
- Add a README: Add a README file to your activity to explain what it does and how to use it.
- Document complex logic: Document complex logic to make it easier to understand.
- Document settings: Document settings to explain what they do and how to use them.
Security
Avoid Exposing Sensitive Information
Avoid exposing sensitive information in your activity, such as user IDs, email addresses, or personal information.
// Good
presenceData.details = 'Logged in'
presenceData.state = 'Viewing profile'
// Bad
presenceData.details = 'Logged in as user123'
presenceData.state = 'Email: user@example.com'
Complete Example
Here's a complete example of an activity that follows best practices:
metadata.json
{
"author": {
"name": "Your Name",
"id": "your_discord_id"
},
"service": "Example",
"description": {
"de": "Example ist eine Website, die etwas Cooles macht.",
"en": "Example is a website that does something cool.",
"fr": "Example est un site web qui fait quelque chose de cool."
},
"url": "example.com",
"version": "1.0.0",
"logo": "https://i.imgur.com/XXXXXXX.png",
"thumbnail": "https://i.imgur.com/YYYYYYY.png",
"color": "#FF0000",
"category": "other",
"tags": ["example", "best-practices"],
"settings": [
{
"id": "showButtons",
"multiLanguage": true,
"value": true
},
{
"id": "showTimestamp",
"multiLanguage": true,
"value": true
},
{
"id": "showDetails",
"multiLanguage": true,
"value": true
}
]
}
presence.ts
import { ActivityType, Assets, getTimestamps } from 'premid'
enum ActivityAssets {
Logo = 'https://i.imgur.com/logo.png',
}
const presence = new Presence({
clientId: 'your_client_id'
})
// Constants
const browsingTimestamp = Math.floor(Date.now() / 1000)
// Cache for heavy computations
const cachedData = null
const lastComputation = 0
// Helper function to get page information
function getPageInfo() {
const { pathname, hostname, href } = document.location
const title = document.title
let pageType = 'unknown'
if (pathname === '/') {
pageType = 'homepage'
}
else if (pathname.includes('/about')) {
pageType = 'about'
}
else if (pathname.includes('/contact')) {
pageType = 'contact'
}
return { pathname, hostname, href, title, pageType }
}
// Helper function to get video information
function getVideoInfo() {
const video = document.querySelector('video')
if (!video || video.readyState === 0) {
return null
}
return {
title: document.querySelector('.video-title')?.textContent || 'Unknown video',
author: document.querySelector('.video-author')?.textContent || 'Unknown author',
isPlaying: !video.paused,
currentTime: video.currentTime,
duration: video.duration
}
}
presence.on('UpdateData', async () => {
// Get settings using Promise.all
const [
showButtons,
showTimestamp,
showDetails
] = await Promise.all([
presence.getSetting<boolean>('showButtons'),
presence.getSetting<boolean>('showTimestamp'),
presence.getSetting<boolean>('showDetails')
])
// Get translations
const strings = await presence.getStrings({
play: 'general.playing',
pause: 'general.paused',
browse: 'general.browsing'
})
// Create the base presence data
const presenceData: PresenceData = {
largeImageKey: ActivityAssets.Logo
}
// Get page information
const { pathname, hostname, href, title, pageType } = getPageInfo()
// Get video information (if available)
const videoInfo = getVideoInfo()
// Set presence data based on page type
if (videoInfo) {
// Video page
presenceData.type = ActivityType.Watching
if (showDetails) {
presenceData.details = videoInfo.title
presenceData.state = `By ${videoInfo.author}`
}
if (videoInfo.isPlaying) {
presenceData.smallImageKey = Assets.Play
presenceData.smallImageText = strings.play
if (showTimestamp) {
[presenceData.startTimestamp, presenceData.endTimestamp]
= getTimestamps(videoInfo.currentTime, videoInfo.duration)
}
}
else {
presenceData.smallImageKey = Assets.Pause
presenceData.smallImageText = strings.pause
}
}
else {
// Regular page
if (showDetails) {
presenceData.details = strings.browse
presenceData.state = title
}
if (showTimestamp) {
presenceData.startTimestamp = browsingTimestamp
}
}
// Add buttons if enabled
if (showButtons) {
presenceData.buttons = [
{
label: 'Visit Website',
url: document.location.href
}
]
}
// Set the activity
if (presenceData.details) {
presence.setActivity(presenceData)
}
else {
presence.clearActivity()
}
})
Validating Your Activity
Before submitting your activity to GitHub, you should validate it to ensure it meets all requirements and follows best practices:
npx pmd build "YourActivityName" --validate
This command will check your activity for common issues and provide feedback on what needs to be fixed.
Conclusion
Following these best practices will help you create high-quality activities that are easy to maintain and provide a good user experience. Remember to:
- Write clean, maintainable code: Use TypeScript, handle errors, use constants, comment your code, and follow the style guide.
- Optimize for performance: Minimize DOM queries, use efficient selectors, and avoid heavy computations.
- Enhance user experience: Provide clear information, use appropriate activity types, add timestamps, use settings, and add multilanguage support.
- Use proper timestamps: Create browsing timestamps outside UpdateData, only update them when changing states, and use destructuring with getTimestamps.
- Use proper image URLs: Use Imgur links for images during development, which will be transferred to the PreMiD CDN when your PR is merged.
- Maintain your activity: Keep your activity updated, write tests, and document your code.
- Ensure security: Avoid exposing sensitive information and validate user input.
- Validate before submission: Always validate your activity with
npx pmd build "YourActivityName" --validate
before submitting it.
By following these practices, you'll create activities that users will love and that will be easy to maintain over time.