Setting Facebook meta tags

A goal for many of the sites we build is for them to be shared on Facebook. The least we can do is make sure they look good when it happens.

The first thing you’ll want to do is run your site through the Facebook Debugger.

I’m currently setting this up for Invoice Ninja. You can see below what our site looked like before setting the meta tags. In the “Open Graph Warnings That Should Be Fixed” section there are a number of properties which are inferred.

Facebook Debugger

To set the metadata (and resolved the warnings) you just need to add the following meta tags to the website, filling in your own values for the content fields.

<meta property="og:url" content=""></meta>
<meta property="og:title" content=""></meta>
<meta property="og:image" content=""></meta>
<meta property="og:description" content=""></meta>

It’s worth noting that after your site gets 50 likes the title becomes fixed.

For a listing of all available tags and options check out the Open Graph documentation at http://ogp.me/.

Friendly URLs with per-account incrementing Ids in Laravel

For Invoice Ninja we wanted each account to be able to use friendly URLs where their Ids would increment.

This would mean the URL for the first client they create would be https://www.invoiceninja.com/clients/1

Many accounts are stored in the same database so we’re not able to use the auto-incrementing Id field. The solution we’re using is to have all classes which store user data (ie, clients, invoices, payments, etc) extend the EntityModel class. To create a new instance you call the createNew() method.

Here are the benefits of this approach:

  • Context is tracked: The account and user the record belong to is automatically set. If an item is created by the system (ie, an automatically sent recurring invoice) the context can be inherited from an existing record.
  • Out of the box security: When setting foreign key values we use getPrivateId() to resolve the public id to a private table Id. This prevents the user from hand modifying the HTML to reference another account’s data.
  • Easy scoping: Using the scope() method we’re able to easily load data filtered by the account. For example Client::scope()->get() returns all of the accounts clients, while Client::scope($publicId)->get() returns a client using its public Id.

And finally the code…

class EntityModel extends Eloquent
{
	protected $softDelete = true;
	public $timestamps = true;
	
	protected $hidden = ['id', 'created_at', 'deleted_at', 'updated_at'];

	public static function createNew($parent = false)
	{		
		$className = get_called_class();
		$entity = new $className();
		
		if ($parent) 
		{
			$entity->user_id = $parent->user_id;
			$entity->account_id = $parent->account_id;
		} 
		else if (Auth::check()) 
		{
			$entity->user_id = Auth::user()->id;
			$entity->account_id = Auth::user()->account_id;			
		} 
		else 
		{
			Utils::fatalError();
		}

		$lastEntity = $className::withTrashed()->scope(false, $entity->account_id)->orderBy('public_id', 'DESC')->first();

		if ($lastEntity)
		{
			$entity->public_id = $lastEntity->public_id + 1;
		}
		else
		{
			$entity->public_id = 1;
		}
		
		return $entity;
	}

	public static function getPrivateId($publicId)
	{
		$className = get_called_class();
		return $className::scope($publicId)->pluck('id');
	}

	public function scopeScope($query, $publicId = false, $accountId = false)
	{
		if (!$accountId) 
		{
			$accountId = Auth::user()->account_id;
		}
		
		$query->whereAccountId($accountId);

		if ($publicId)
		{
			if (is_array($publicId))
			{
				$query->whereIn('public_id', $publicId);
			}
			else
			{
				$query->wherePublicId($publicId);
			}
		}
		
		return $query;
	}
}

I’m sure there are other ways to tackle this problem. Let me know in the comments or on twitter @hillelcoren if you have a different solution.

Logging errors in Laravel

Laravel comes with a really helpful Log::error() function to write messages to the log file. Here’s how you can use it to track PHP and JavaScript errors along with useful contextual information to help troubleshoot problems.

The Log::error() function accepts a second optional parameter allowing you to specify additional info. To make setting this easier create a logError() function in a Utils class.

public static function logError($error, $code = '', $context = 'PHP')
{
	$count = Session::get('error_count', 0);
	Session::put('error_count', ++$count);

	$data = [
		'context' => $context,
		'user_id' => Auth::check() ? Auth::user()->id : 0,
		'user_name' => Auth::check() ? Auth::user()->getDisplayName() : '',
		'url' => Request::url(),
		'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
		'ip' => Request::getClientIp(),
		'count' => $count,
		'code' => $code
	];

	Log::error($error, $data);
}

Most of the code should be self explanatory. getDisplayName() is a function I add to the User class (as well as other classes) where there is logic involved in determining the name. In this case it may be the user’s full name or email address.

To configure Laravel to use this function by default change the App::error() function in app/start/global.php to the following.

App::error(function(Exception $exception, $code)
{
	Utils::logError($exception, $code);
});

To catch all JavaScript errors enable accessing the Utils::logError() function by adding this to your routes.php file.

Route::get('/log_error', function() 
{
	return Utils::logError(Input::get('error'), '', 'JavaScript');
});

And finally, add the following to function to your master template file.

window.onerror = function(e) {
	try {
		$.ajax({
			type: 'GET',
			url: '{{ URL::to('log_error') }}',
			data: 'error='+e
		});     
	} catch(err) {}
	return false;
}